datepicker.js 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870
  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v0.11.4
  6. */
  7. goog.provide('ng.material.components.datepicker');
  8. goog.require('ng.material.components.icon');
  9. goog.require('ng.material.components.virtualRepeat');
  10. goog.require('ng.material.core');
  11. (function() {
  12. 'use strict';
  13. /**
  14. * @ngdoc module
  15. * @name material.components.datepicker
  16. * @description Datepicker
  17. */
  18. angular.module('material.components.datepicker', [
  19. 'material.core',
  20. 'material.components.icon',
  21. 'material.components.virtualRepeat'
  22. ]).directive('mdCalendar', calendarDirective);
  23. // POST RELEASE
  24. // TODO(jelbourn): Mac Cmd + left / right == Home / End
  25. // TODO(jelbourn): Clicking on the month label opens the month-picker.
  26. // TODO(jelbourn): Minimum and maximum date
  27. // TODO(jelbourn): Refactor month element creation to use cloneNode (performance).
  28. // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override.
  29. // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat)
  30. // TODO(jelbourn): Scroll snapping (virtual repeat)
  31. // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat)
  32. // TODO(jelbourn): Month headers stick to top when scrolling.
  33. // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view.
  34. // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live
  35. // announcement and key handling).
  36. // Read-only calendar (not just date-picker).
  37. /**
  38. * Height of one calendar month tbody. This must be made known to the virtual-repeat and is
  39. * subsequently used for scrolling to specific months.
  40. */
  41. var TBODY_HEIGHT = 265;
  42. /**
  43. * Height of a calendar month with a single row. This is needed to calculate the offset for
  44. * rendering an extra month in virtual-repeat that only contains one row.
  45. */
  46. var TBODY_SINGLE_ROW_HEIGHT = 45;
  47. function calendarDirective() {
  48. return {
  49. template:
  50. '<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' +
  51. '<div class="md-calendar-scroll-mask">' +
  52. '<md-virtual-repeat-container class="md-calendar-scroll-container" ' +
  53. 'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' +
  54. '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
  55. '<tbody role="rowgroup" md-virtual-repeat="i in ctrl.items" md-calendar-month ' +
  56. 'md-month-offset="$index" class="md-calendar-month" ' +
  57. 'md-start-index="ctrl.getSelectedMonthIndex()" ' +
  58. 'md-item-size="' + TBODY_HEIGHT + '"></tbody>' +
  59. '</table>' +
  60. '</md-virtual-repeat-container>' +
  61. '</div>',
  62. scope: {
  63. minDate: '=mdMinDate',
  64. maxDate: '=mdMaxDate',
  65. },
  66. require: ['ngModel', 'mdCalendar'],
  67. controller: CalendarCtrl,
  68. controllerAs: 'ctrl',
  69. bindToController: true,
  70. link: function(scope, element, attrs, controllers) {
  71. var ngModelCtrl = controllers[0];
  72. var mdCalendarCtrl = controllers[1];
  73. mdCalendarCtrl.configureNgModel(ngModelCtrl);
  74. }
  75. };
  76. }
  77. /** Class applied to the selected date cell/. */
  78. var SELECTED_DATE_CLASS = 'md-calendar-selected-date';
  79. /** Class applied to the focused date cell/. */
  80. var FOCUSED_DATE_CLASS = 'md-focus';
  81. /** Next identifier for calendar instance. */
  82. var nextUniqueId = 0;
  83. /** The first renderable date in the virtual-scrolling calendar (for all instances). */
  84. var firstRenderableDate = null;
  85. /**
  86. * Controller for the mdCalendar component.
  87. * ngInject @constructor
  88. */
  89. function CalendarCtrl($element, $attrs, $scope, $animate, $q, $mdConstant,
  90. $mdTheming, $$mdDateUtil, $mdDateLocale, $mdInkRipple, $mdUtil) {
  91. $mdTheming($element);
  92. /**
  93. * Dummy array-like object for virtual-repeat to iterate over. The length is the total
  94. * number of months that can be viewed. This is shorter than ideal because of (potential)
  95. * Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1181658.
  96. */
  97. this.items = {length: 2000};
  98. if (this.maxDate && this.minDate) {
  99. // Limit the number of months if min and max dates are set.
  100. var numMonths = $$mdDateUtil.getMonthDistance(this.minDate, this.maxDate) + 1;
  101. numMonths = Math.max(numMonths, 1);
  102. // Add an additional month as the final dummy month for rendering purposes.
  103. numMonths += 1;
  104. this.items.length = numMonths;
  105. }
  106. /** @final {!angular.$animate} */
  107. this.$animate = $animate;
  108. /** @final {!angular.$q} */
  109. this.$q = $q;
  110. /** @final */
  111. this.$mdInkRipple = $mdInkRipple;
  112. /** @final */
  113. this.$mdUtil = $mdUtil;
  114. /** @final */
  115. this.keyCode = $mdConstant.KEY_CODE;
  116. /** @final */
  117. this.dateUtil = $$mdDateUtil;
  118. /** @final */
  119. this.dateLocale = $mdDateLocale;
  120. /** @final {!angular.JQLite} */
  121. this.$element = $element;
  122. /** @final {!angular.Scope} */
  123. this.$scope = $scope;
  124. /** @final {HTMLElement} */
  125. this.calendarElement = $element[0].querySelector('.md-calendar');
  126. /** @final {HTMLElement} */
  127. this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
  128. /** @final {Date} */
  129. this.today = this.dateUtil.createDateAtMidnight();
  130. /** @type {Date} */
  131. this.firstRenderableDate = this.dateUtil.incrementMonths(this.today, -this.items.length / 2);
  132. if (this.minDate && this.minDate > this.firstRenderableDate) {
  133. this.firstRenderableDate = this.minDate;
  134. } else if (this.maxDate) {
  135. // Calculate the difference between the start date and max date.
  136. // Subtract 1 because it's an inclusive difference and 1 for the final dummy month.
  137. //
  138. var monthDifference = this.items.length - 2;
  139. this.firstRenderableDate = this.dateUtil.incrementMonths(this.maxDate, -(this.items.length - 2));
  140. }
  141. /** @final {number} Unique ID for this calendar instance. */
  142. this.id = nextUniqueId++;
  143. /** @type {!angular.NgModelController} */
  144. this.ngModelCtrl = null;
  145. /**
  146. * The selected date. Keep track of this separately from the ng-model value so that we
  147. * can know, when the ng-model value changes, what the previous value was before its updated
  148. * in the component's UI.
  149. *
  150. * @type {Date}
  151. */
  152. this.selectedDate = null;
  153. /**
  154. * The date that is currently focused or showing in the calendar. This will initially be set
  155. * to the ng-model value if set, otherwise to today. It will be updated as the user navigates
  156. * to other months. The cell corresponding to the displayDate does not necesarily always have
  157. * focus in the document (such as for cases when the user is scrolling the calendar).
  158. * @type {Date}
  159. */
  160. this.displayDate = null;
  161. /**
  162. * The date that has or should have focus.
  163. * @type {Date}
  164. */
  165. this.focusDate = null;
  166. /** @type {boolean} */
  167. this.isInitialized = false;
  168. /** @type {boolean} */
  169. this.isMonthTransitionInProgress = false;
  170. // Unless the user specifies so, the calendar should not be a tab stop.
  171. // This is necessary because ngAria might add a tabindex to anything with an ng-model
  172. // (based on whether or not the user has turned that particular feature on/off).
  173. if (!$attrs['tabindex']) {
  174. $element.attr('tabindex', '-1');
  175. }
  176. var self = this;
  177. /**
  178. * Handles a click event on a date cell.
  179. * Created here so that every cell can use the same function instance.
  180. * @this {HTMLTableCellElement} The cell that was clicked.
  181. */
  182. this.cellClickHandler = function() {
  183. var cellElement = this;
  184. if (this.hasAttribute('data-timestamp')) {
  185. $scope.$apply(function() {
  186. var timestamp = Number(cellElement.getAttribute('data-timestamp'));
  187. self.setNgModelValue(self.dateUtil.createDateAtMidnight(timestamp));
  188. });
  189. }
  190. };
  191. this.attachCalendarEventListeners();
  192. }
  193. CalendarCtrl.$inject = ["$element", "$attrs", "$scope", "$animate", "$q", "$mdConstant", "$mdTheming", "$$mdDateUtil", "$mdDateLocale", "$mdInkRipple", "$mdUtil"];
  194. /*** Initialization ***/
  195. /**
  196. * Sets up the controller's reference to ngModelController.
  197. * @param {!angular.NgModelController} ngModelCtrl
  198. */
  199. CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) {
  200. this.ngModelCtrl = ngModelCtrl;
  201. var self = this;
  202. ngModelCtrl.$render = function() {
  203. self.changeSelectedDate(self.ngModelCtrl.$viewValue);
  204. };
  205. };
  206. /**
  207. * Initialize the calendar by building the months that are initially visible.
  208. * Initialization should occur after the ngModel value is known.
  209. */
  210. CalendarCtrl.prototype.buildInitialCalendarDisplay = function() {
  211. this.buildWeekHeader();
  212. this.hideVerticalScrollbar();
  213. this.displayDate = this.selectedDate || this.today;
  214. this.isInitialized = true;
  215. };
  216. /**
  217. * Hides the vertical scrollbar on the calendar scroller by setting the width on the
  218. * calendar scroller and the `overflow: hidden` wrapper around the scroller, and then setting
  219. * a padding-right on the scroller equal to the width of the browser's scrollbar.
  220. *
  221. * This will cause a reflow.
  222. */
  223. CalendarCtrl.prototype.hideVerticalScrollbar = function() {
  224. var element = this.$element[0];
  225. var scrollMask = element.querySelector('.md-calendar-scroll-mask');
  226. var scroller = this.calendarScroller;
  227. var headerWidth = element.querySelector('.md-calendar-day-header').clientWidth;
  228. var scrollbarWidth = scroller.offsetWidth - scroller.clientWidth;
  229. scrollMask.style.width = headerWidth + 'px';
  230. scroller.style.width = (headerWidth + scrollbarWidth) + 'px';
  231. scroller.style.paddingRight = scrollbarWidth + 'px';
  232. };
  233. /** Attach event listeners for the calendar. */
  234. CalendarCtrl.prototype.attachCalendarEventListeners = function() {
  235. // Keyboard interaction.
  236. this.$element.on('keydown', angular.bind(this, this.handleKeyEvent));
  237. };
  238. /*** User input handling ***/
  239. /**
  240. * Handles a key event in the calendar with the appropriate action. The action will either
  241. * be to select the focused date or to navigate to focus a new date.
  242. * @param {KeyboardEvent} event
  243. */
  244. CalendarCtrl.prototype.handleKeyEvent = function(event) {
  245. var self = this;
  246. this.$scope.$apply(function() {
  247. // Capture escape and emit back up so that a wrapping component
  248. // (such as a date-picker) can decide to close.
  249. if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) {
  250. self.$scope.$emit('md-calendar-close');
  251. if (event.which == self.keyCode.TAB) {
  252. event.preventDefault();
  253. }
  254. return;
  255. }
  256. // Remaining key events fall into two categories: selection and navigation.
  257. // Start by checking if this is a selection event.
  258. if (event.which === self.keyCode.ENTER) {
  259. self.setNgModelValue(self.displayDate);
  260. event.preventDefault();
  261. return;
  262. }
  263. // Selection isn't occuring, so the key event is either navigation or nothing.
  264. var date = self.getFocusDateFromKeyEvent(event);
  265. if (date) {
  266. date = self.boundDateByMinAndMax(date);
  267. event.preventDefault();
  268. event.stopPropagation();
  269. // Since this is a keyboard interaction, actually give the newly focused date keyboard
  270. // focus after the been brought into view.
  271. self.changeDisplayDate(date).then(function () {
  272. self.focus(date);
  273. });
  274. }
  275. });
  276. };
  277. /**
  278. * Gets the date to focus as the result of a key event.
  279. * @param {KeyboardEvent} event
  280. * @returns {Date} Date to navigate to, or null if the key does not match a calendar shortcut.
  281. */
  282. CalendarCtrl.prototype.getFocusDateFromKeyEvent = function(event) {
  283. var dateUtil = this.dateUtil;
  284. var keyCode = this.keyCode;
  285. switch (event.which) {
  286. case keyCode.RIGHT_ARROW: return dateUtil.incrementDays(this.displayDate, 1);
  287. case keyCode.LEFT_ARROW: return dateUtil.incrementDays(this.displayDate, -1);
  288. case keyCode.DOWN_ARROW:
  289. return event.metaKey ?
  290. dateUtil.incrementMonths(this.displayDate, 1) :
  291. dateUtil.incrementDays(this.displayDate, 7);
  292. case keyCode.UP_ARROW:
  293. return event.metaKey ?
  294. dateUtil.incrementMonths(this.displayDate, -1) :
  295. dateUtil.incrementDays(this.displayDate, -7);
  296. case keyCode.PAGE_DOWN: return dateUtil.incrementMonths(this.displayDate, 1);
  297. case keyCode.PAGE_UP: return dateUtil.incrementMonths(this.displayDate, -1);
  298. case keyCode.HOME: return dateUtil.getFirstDateOfMonth(this.displayDate);
  299. case keyCode.END: return dateUtil.getLastDateOfMonth(this.displayDate);
  300. default: return null;
  301. }
  302. };
  303. /**
  304. * Gets the "index" of the currently selected date as it would be in the virtual-repeat.
  305. * @returns {number}
  306. */
  307. CalendarCtrl.prototype.getSelectedMonthIndex = function() {
  308. return this.dateUtil.getMonthDistance(this.firstRenderableDate,
  309. this.selectedDate || this.today);
  310. };
  311. /**
  312. * Scrolls to the month of the given date.
  313. * @param {Date} date
  314. */
  315. CalendarCtrl.prototype.scrollToMonth = function(date) {
  316. if (!this.dateUtil.isValidDate(date)) {
  317. return;
  318. }
  319. var monthDistance = this.dateUtil.getMonthDistance(this.firstRenderableDate, date);
  320. this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
  321. };
  322. /**
  323. * Sets the ng-model value for the calendar and emits a change event.
  324. * @param {Date} date
  325. */
  326. CalendarCtrl.prototype.setNgModelValue = function(date) {
  327. this.$scope.$emit('md-calendar-change', date);
  328. this.ngModelCtrl.$setViewValue(date);
  329. this.ngModelCtrl.$render();
  330. };
  331. /**
  332. * Focus the cell corresponding to the given date.
  333. * @param {Date=} opt_date
  334. */
  335. CalendarCtrl.prototype.focus = function(opt_date) {
  336. var date = opt_date || this.selectedDate || this.today;
  337. var previousFocus = this.calendarElement.querySelector('.md-focus');
  338. if (previousFocus) {
  339. previousFocus.classList.remove(FOCUSED_DATE_CLASS);
  340. }
  341. var cellId = this.getDateId(date);
  342. var cell = document.getElementById(cellId);
  343. if (cell) {
  344. cell.classList.add(FOCUSED_DATE_CLASS);
  345. cell.focus();
  346. } else {
  347. this.focusDate = date;
  348. }
  349. };
  350. /**
  351. * If a date exceeds minDate or maxDate, returns date matching minDate or maxDate, respectively.
  352. * Otherwise, returns the date.
  353. * @param {Date} date
  354. * @return {Date}
  355. */
  356. CalendarCtrl.prototype.boundDateByMinAndMax = function(date) {
  357. var boundDate = date;
  358. if (this.minDate && date < this.minDate) {
  359. boundDate = new Date(this.minDate.getTime());
  360. }
  361. if (this.maxDate && date > this.maxDate) {
  362. boundDate = new Date(this.maxDate.getTime());
  363. }
  364. return boundDate;
  365. };
  366. /*** Updating the displayed / selected date ***/
  367. /**
  368. * Change the selected date in the calendar (ngModel value has already been changed).
  369. * @param {Date} date
  370. */
  371. CalendarCtrl.prototype.changeSelectedDate = function(date) {
  372. var self = this;
  373. var previousSelectedDate = this.selectedDate;
  374. this.selectedDate = date;
  375. this.changeDisplayDate(date).then(function() {
  376. // Remove the selected class from the previously selected date, if any.
  377. if (previousSelectedDate) {
  378. var prevDateCell =
  379. document.getElementById(self.getDateId(previousSelectedDate));
  380. if (prevDateCell) {
  381. prevDateCell.classList.remove(SELECTED_DATE_CLASS);
  382. prevDateCell.setAttribute('aria-selected', 'false');
  383. }
  384. }
  385. // Apply the select class to the new selected date if it is set.
  386. if (date) {
  387. var dateCell = document.getElementById(self.getDateId(date));
  388. if (dateCell) {
  389. dateCell.classList.add(SELECTED_DATE_CLASS);
  390. dateCell.setAttribute('aria-selected', 'true');
  391. }
  392. }
  393. });
  394. };
  395. /**
  396. * Change the date that is being shown in the calendar. If the given date is in a different
  397. * month, the displayed month will be transitioned.
  398. * @param {Date} date
  399. */
  400. CalendarCtrl.prototype.changeDisplayDate = function(date) {
  401. // Initialization is deferred until this function is called because we want to reflect
  402. // the starting value of ngModel.
  403. if (!this.isInitialized) {
  404. this.buildInitialCalendarDisplay();
  405. return this.$q.when();
  406. }
  407. // If trying to show an invalid date or a transition is in progress, do nothing.
  408. if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) {
  409. return this.$q.when();
  410. }
  411. this.isMonthTransitionInProgress = true;
  412. var animationPromise = this.animateDateChange(date);
  413. this.displayDate = date;
  414. var self = this;
  415. animationPromise.then(function() {
  416. self.isMonthTransitionInProgress = false;
  417. });
  418. return animationPromise;
  419. };
  420. /**
  421. * Animates the transition from the calendar's current month to the given month.
  422. * @param {Date} date
  423. * @returns {angular.$q.Promise} The animation promise.
  424. */
  425. CalendarCtrl.prototype.animateDateChange = function(date) {
  426. this.scrollToMonth(date);
  427. return this.$q.when();
  428. };
  429. /*** Constructing the calendar table ***/
  430. /**
  431. * Builds and appends a day-of-the-week header to the calendar.
  432. * This should only need to be called once during initialization.
  433. */
  434. CalendarCtrl.prototype.buildWeekHeader = function() {
  435. var firstDayOfWeek = this.dateLocale.firstDayOfWeek;
  436. var shortDays = this.dateLocale.shortDays;
  437. var row = document.createElement('tr');
  438. for (var i = 0; i < 7; i++) {
  439. var th = document.createElement('th');
  440. th.textContent = shortDays[(i + firstDayOfWeek) % 7];
  441. row.appendChild(th);
  442. }
  443. this.$element.find('thead').append(row);
  444. };
  445. /**
  446. * Gets an identifier for a date unique to the calendar instance for internal
  447. * purposes. Not to be displayed.
  448. * @param {Date} date
  449. * @returns {string}
  450. */
  451. CalendarCtrl.prototype.getDateId = function(date) {
  452. return [
  453. 'md',
  454. this.id,
  455. date.getFullYear(),
  456. date.getMonth(),
  457. date.getDate()
  458. ].join('-');
  459. };
  460. })();
  461. (function() {
  462. 'use strict';
  463. angular.module('material.components.datepicker')
  464. .directive('mdCalendarMonth', mdCalendarMonthDirective);
  465. /**
  466. * Private directive consumed by md-calendar. Having this directive lets the calender use
  467. * md-virtual-repeat and also cleanly separates the month DOM construction functions from
  468. * the rest of the calendar controller logic.
  469. */
  470. function mdCalendarMonthDirective() {
  471. return {
  472. require: ['^^mdCalendar', 'mdCalendarMonth'],
  473. scope: {offset: '=mdMonthOffset'},
  474. controller: CalendarMonthCtrl,
  475. controllerAs: 'mdMonthCtrl',
  476. bindToController: true,
  477. link: function(scope, element, attrs, controllers) {
  478. var calendarCtrl = controllers[0];
  479. var monthCtrl = controllers[1];
  480. monthCtrl.calendarCtrl = calendarCtrl;
  481. monthCtrl.generateContent();
  482. // The virtual-repeat re-uses the same DOM elements, so there are only a limited number
  483. // of repeated items that are linked, and then those elements have their bindings updataed.
  484. // Since the months are not generated by bindings, we simply regenerate the entire thing
  485. // when the binding (offset) changes.
  486. scope.$watch(function() { return monthCtrl.offset; }, function(offset, oldOffset) {
  487. if (offset != oldOffset) {
  488. monthCtrl.generateContent();
  489. }
  490. });
  491. }
  492. };
  493. }
  494. /** Class applied to the cell for today. */
  495. var TODAY_CLASS = 'md-calendar-date-today';
  496. /** Class applied to the selected date cell/. */
  497. var SELECTED_DATE_CLASS = 'md-calendar-selected-date';
  498. /** Class applied to the focused date cell/. */
  499. var FOCUSED_DATE_CLASS = 'md-focus';
  500. /**
  501. * Controller for a single calendar month.
  502. * ngInject @constructor
  503. */
  504. function CalendarMonthCtrl($element, $$mdDateUtil, $mdDateLocale) {
  505. this.dateUtil = $$mdDateUtil;
  506. this.dateLocale = $mdDateLocale;
  507. this.$element = $element;
  508. this.calendarCtrl = null;
  509. /**
  510. * Number of months from the start of the month "items" that the currently rendered month
  511. * occurs. Set via angular data binding.
  512. * @type {number}
  513. */
  514. this.offset;
  515. /**
  516. * Date cell to focus after appending the month to the document.
  517. * @type {HTMLElement}
  518. */
  519. this.focusAfterAppend = null;
  520. }
  521. CalendarMonthCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"];
  522. /** Generate and append the content for this month to the directive element. */
  523. CalendarMonthCtrl.prototype.generateContent = function() {
  524. var calendarCtrl = this.calendarCtrl;
  525. var date = this.dateUtil.incrementMonths(calendarCtrl.firstRenderableDate, this.offset);
  526. this.$element.empty();
  527. this.$element.append(this.buildCalendarForMonth(date));
  528. if (this.focusAfterAppend) {
  529. this.focusAfterAppend.classList.add(FOCUSED_DATE_CLASS);
  530. this.focusAfterAppend.focus();
  531. this.focusAfterAppend = null;
  532. }
  533. };
  534. /**
  535. * Creates a single cell to contain a date in the calendar with all appropriate
  536. * attributes and classes added. If a date is given, the cell content will be set
  537. * based on the date.
  538. * @param {Date=} opt_date
  539. * @returns {HTMLElement}
  540. */
  541. CalendarMonthCtrl.prototype.buildDateCell = function(opt_date) {
  542. var calendarCtrl = this.calendarCtrl;
  543. // TODO(jelbourn): cloneNode is likely a faster way of doing this.
  544. var cell = document.createElement('td');
  545. cell.tabIndex = -1;
  546. cell.classList.add('md-calendar-date');
  547. cell.setAttribute('role', 'gridcell');
  548. if (opt_date) {
  549. cell.setAttribute('tabindex', '-1');
  550. cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date));
  551. cell.id = calendarCtrl.getDateId(opt_date);
  552. // Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
  553. cell.setAttribute('data-timestamp', opt_date.getTime());
  554. // TODO(jelourn): Doing these comparisons for class addition during generation might be slow.
  555. // It may be better to finish the construction and then query the node and add the class.
  556. if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) {
  557. cell.classList.add(TODAY_CLASS);
  558. }
  559. if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
  560. this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) {
  561. cell.classList.add(SELECTED_DATE_CLASS);
  562. cell.setAttribute('aria-selected', 'true');
  563. }
  564. var cellText = this.dateLocale.dates[opt_date.getDate()];
  565. if (this.dateUtil.isDateWithinRange(opt_date,
  566. this.calendarCtrl.minDate, this.calendarCtrl.maxDate)) {
  567. // Add a indicator for select, hover, and focus states.
  568. var selectionIndicator = document.createElement('span');
  569. cell.appendChild(selectionIndicator);
  570. selectionIndicator.classList.add('md-calendar-date-selection-indicator');
  571. selectionIndicator.textContent = cellText;
  572. cell.addEventListener('click', calendarCtrl.cellClickHandler);
  573. if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) {
  574. this.focusAfterAppend = cell;
  575. }
  576. } else {
  577. cell.classList.add('md-calendar-date-disabled');
  578. cell.textContent = cellText;
  579. }
  580. }
  581. return cell;
  582. };
  583. /**
  584. * Builds a `tr` element for the calendar grid.
  585. * @param rowNumber The week number within the month.
  586. * @returns {HTMLElement}
  587. */
  588. CalendarMonthCtrl.prototype.buildDateRow = function(rowNumber) {
  589. var row = document.createElement('tr');
  590. row.setAttribute('role', 'row');
  591. // Because of an NVDA bug (with Firefox), the row needs an aria-label in order
  592. // to prevent the entire row being read aloud when the user moves between rows.
  593. // See http://community.nvda-project.org/ticket/4643.
  594. row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber));
  595. return row;
  596. };
  597. /**
  598. * Builds the <tbody> content for the given date's month.
  599. * @param {Date=} opt_dateInMonth
  600. * @returns {DocumentFragment} A document fragment containing the <tr> elements.
  601. */
  602. CalendarMonthCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) {
  603. var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date();
  604. var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date);
  605. var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth);
  606. var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date);
  607. // Store rows for the month in a document fragment so that we can append them all at once.
  608. var monthBody = document.createDocumentFragment();
  609. var rowNumber = 1;
  610. var row = this.buildDateRow(rowNumber);
  611. monthBody.appendChild(row);
  612. // If this is the final month in the list of items, only the first week should render,
  613. // so we should return immediately after the first row is complete and has been
  614. // attached to the body.
  615. var isFinalMonth = this.offset === this.calendarCtrl.items.length - 1;
  616. // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label
  617. // goes on a row above the first of the month. Otherwise, the month label takes up the first
  618. // two cells of the first row.
  619. var blankCellOffset = 0;
  620. var monthLabelCell = document.createElement('td');
  621. monthLabelCell.classList.add('md-calendar-month-label');
  622. // If the entire month is after the max date, render the label as a disabled state.
  623. if (this.calendarCtrl.maxDate && firstDayOfMonth > this.calendarCtrl.maxDate) {
  624. monthLabelCell.classList.add('md-calendar-month-label-disabled');
  625. }
  626. monthLabelCell.textContent = this.dateLocale.monthHeaderFormatter(date);
  627. if (firstDayOfTheWeek <= 2) {
  628. monthLabelCell.setAttribute('colspan', '7');
  629. var monthLabelRow = this.buildDateRow();
  630. monthLabelRow.appendChild(monthLabelCell);
  631. monthBody.insertBefore(monthLabelRow, row);
  632. if (isFinalMonth) {
  633. return monthBody;
  634. }
  635. } else {
  636. blankCellOffset = 2;
  637. monthLabelCell.setAttribute('colspan', '2');
  638. row.appendChild(monthLabelCell);
  639. }
  640. // Add a blank cell for each day of the week that occurs before the first of the month.
  641. // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon.
  642. // The blankCellOffset is needed in cases where the first N cells are used by the month label.
  643. for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) {
  644. row.appendChild(this.buildDateCell());
  645. }
  646. // Add a cell for each day of the month, keeping track of the day of the week so that
  647. // we know when to start a new row.
  648. var dayOfWeek = firstDayOfTheWeek;
  649. var iterationDate = firstDayOfMonth;
  650. for (var d = 1; d <= numberOfDaysInMonth; d++) {
  651. // If we've reached the end of the week, start a new row.
  652. if (dayOfWeek === 7) {
  653. // We've finished the first row, so we're done if this is the final month.
  654. if (isFinalMonth) {
  655. return monthBody;
  656. }
  657. dayOfWeek = 0;
  658. rowNumber++;
  659. row = this.buildDateRow(rowNumber);
  660. monthBody.appendChild(row);
  661. }
  662. iterationDate.setDate(d);
  663. var cell = this.buildDateCell(iterationDate);
  664. row.appendChild(cell);
  665. dayOfWeek++;
  666. }
  667. // Ensure that the last row of the month has 7 cells.
  668. while (row.childNodes.length < 7) {
  669. row.appendChild(this.buildDateCell());
  670. }
  671. // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat
  672. // requires that all items have exactly the same height.
  673. while (monthBody.childNodes.length < 6) {
  674. var whitespaceRow = this.buildDateRow();
  675. for (var i = 0; i < 7; i++) {
  676. whitespaceRow.appendChild(this.buildDateCell());
  677. }
  678. monthBody.appendChild(whitespaceRow);
  679. }
  680. return monthBody;
  681. };
  682. /**
  683. * Gets the day-of-the-week index for a date for the current locale.
  684. * @private
  685. * @param {Date} date
  686. * @returns {number} The column index of the date in the calendar.
  687. */
  688. CalendarMonthCtrl.prototype.getLocaleDay_ = function(date) {
  689. return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7
  690. };
  691. })();
  692. (function() {
  693. 'use strict';
  694. /**
  695. * @ngdoc service
  696. * @name $mdDateLocaleProvider
  697. * @module material.components.datepicker
  698. *
  699. * @description
  700. * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service.
  701. * This provider that allows the user to specify messages, formatters, and parsers for date
  702. * internationalization. The `$mdDateLocale` service itself is consumed by Angular Material
  703. * components that that deal with dates.
  704. *
  705. * @property {(Array<string>)=} months Array of month names (in order).
  706. * @property {(Array<string>)=} shortMonths Array of abbreviated month names.
  707. * @property {(Array<string>)=} days Array of the days of the week (in order).
  708. * @property {(Array<string>)=} shortDays Array of abbreviated dayes of the week.
  709. * @property {(Array<string>)=} dates Array of dates of the month. Only necessary for locales
  710. * using a numeral system other than [1, 2, 3...].
  711. * @property {(Array<string>)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1,
  712. * etc.
  713. * @property {(function(string): Date)=} parseDate Function to parse a date object from a string.
  714. * @property {(function(Date): string)=} formatDate Function to format a date object to a string.
  715. * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for
  716. * a month given a date.
  717. * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for
  718. * a week given the week number.
  719. * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale.
  720. * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the
  721. * current locale.
  722. *
  723. * @usage
  724. * <hljs lang="js">
  725. * myAppModule.config(function($mdDateLocaleProvider) {
  726. *
  727. * // Example of a French localization.
  728. * $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...];
  729. * $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...];
  730. * $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...];
  731. * $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...];
  732. *
  733. * // Can change week display to start on Monday.
  734. * $mdDateLocaleProvider.firstDayOfWeek = 1;
  735. *
  736. * // Optional.
  737. * $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...];
  738. *
  739. * // Example uses moment.js to parse and format dates.
  740. * $mdDateLocaleProvider.parseDate = function(dateString) {
  741. * var m = moment(dateString, 'L', true);
  742. * return m.isValid() ? m.toDate() : new Date(NaN);
  743. * };
  744. *
  745. * $mdDateLocaleProvider.formatDate = function(date) {
  746. * return moment(date).format('L');
  747. * };
  748. *
  749. * $mdDateLocaleProvider.monthHeaderFormatter = function(date) {
  750. * return myShortMonths[date.getMonth()] + ' ' + date.getFullYear();
  751. * };
  752. *
  753. * // In addition to date display, date components also need localized messages
  754. * // for aria-labels for screen-reader users.
  755. *
  756. * $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) {
  757. * return 'Semaine ' + weekNumber;
  758. * };
  759. *
  760. * $mdDateLocaleProvider.msgCalendar = 'Calendrier';
  761. * $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier';
  762. *
  763. * });
  764. * </hljs>
  765. *
  766. */
  767. angular.module('material.components.datepicker').config(["$provide", function($provide) {
  768. // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions.
  769. /** @constructor */
  770. function DateLocaleProvider() {
  771. /** Array of full month names. E.g., ['January', 'Febuary', ...] */
  772. this.months = null;
  773. /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */
  774. this.shortMonths = null;
  775. /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */
  776. this.days = null;
  777. /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */
  778. this.shortDays = null;
  779. /** Array of dates of a month (1 - 31). Characters might be different in some locales. */
  780. this.dates = null;
  781. /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */
  782. this.firstDayOfWeek = 0;
  783. /**
  784. * Function that converts the date portion of a Date to a string.
  785. * @type {(function(Date): string)}
  786. */
  787. this.formatDate = null;
  788. /**
  789. * Function that converts a date string to a Date object (the date portion)
  790. * @type {function(string): Date}
  791. */
  792. this.parseDate = null;
  793. /**
  794. * Function that formats a Date into a month header string.
  795. * @type {function(Date): string}
  796. */
  797. this.monthHeaderFormatter = null;
  798. /**
  799. * Function that formats a week number into a label for the week.
  800. * @type {function(number): string}
  801. */
  802. this.weekNumberFormatter = null;
  803. /**
  804. * Function that formats a date into a long aria-label that is read
  805. * when the focused date changes.
  806. * @type {function(Date): string}
  807. */
  808. this.longDateFormatter = null;
  809. /**
  810. * ARIA label for the calendar "dialog" used in the datepicker.
  811. * @type {string}
  812. */
  813. this.msgCalendar = '';
  814. /**
  815. * ARIA label for the datepicker's "Open calendar" buttons.
  816. * @type {string}
  817. */
  818. this.msgOpenCalendar = '';
  819. }
  820. /**
  821. * Factory function that returns an instance of the dateLocale service.
  822. * ngInject
  823. * @param $locale
  824. * @returns {DateLocale}
  825. */
  826. DateLocaleProvider.prototype.$get = function($locale) {
  827. /**
  828. * Default date-to-string formatting function.
  829. * @param {!Date} date
  830. * @returns {string}
  831. */
  832. function defaultFormatDate(date) {
  833. if (!date) {
  834. return '';
  835. }
  836. // All of the dates created through ng-material *should* be set to midnight.
  837. // If we encounter a date where the localeTime shows at 11pm instead of midnight,
  838. // we have run into an issue with DST where we need to increment the hour by one:
  839. // var d = new Date(1992, 9, 8, 0, 0, 0);
  840. // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM"
  841. var localeTime = date.toLocaleTimeString();
  842. var formatDate = date;
  843. if (date.getHours() == 0 &&
  844. (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) {
  845. formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0);
  846. }
  847. return formatDate.toLocaleDateString();
  848. }
  849. /**
  850. * Default string-to-date parsing function.
  851. * @param {string} dateString
  852. * @returns {!Date}
  853. */
  854. function defaultParseDate(dateString) {
  855. return new Date(dateString);
  856. }
  857. /**
  858. * Default function to determine whether a string makes sense to be
  859. * parsed to a Date object.
  860. *
  861. * This is very permissive and is just a basic sanity check to ensure that
  862. * things like single integers aren't able to be parsed into dates.
  863. * @param {string} dateString
  864. * @returns {boolean}
  865. */
  866. function defaultIsDateComplete(dateString) {
  867. dateString = dateString.trim();
  868. // Looks for three chunks of content (either numbers or text) separated
  869. // by delimiters.
  870. var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/;
  871. return re.test(dateString);
  872. }
  873. /**
  874. * Default date-to-string formatter to get a month header.
  875. * @param {!Date} date
  876. * @returns {string}
  877. */
  878. function defaultMonthHeaderFormatter(date) {
  879. return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear();
  880. }
  881. /**
  882. * Default week number formatter.
  883. * @param number
  884. * @returns {string}
  885. */
  886. function defaultWeekNumberFormatter(number) {
  887. return 'Week ' + number;
  888. }
  889. /**
  890. * Default formatter for date cell aria-labels.
  891. * @param {!Date} date
  892. * @returns {string}
  893. */
  894. function defaultLongDateFormatter(date) {
  895. // Example: 'Thursday June 18 2015'
  896. return [
  897. service.days[date.getDay()],
  898. service.months[date.getMonth()],
  899. service.dates[date.getDate()],
  900. date.getFullYear()
  901. ].join(' ');
  902. }
  903. // The default "short" day strings are the first character of each day,
  904. // e.g., "Monday" => "M".
  905. var defaultShortDays = $locale.DATETIME_FORMATS.DAY.map(function(day) {
  906. return day[0];
  907. });
  908. // The default dates are simply the numbers 1 through 31.
  909. var defaultDates = Array(32);
  910. for (var i = 1; i <= 31; i++) {
  911. defaultDates[i] = i;
  912. }
  913. // Default ARIA messages are in English (US).
  914. var defaultMsgCalendar = 'Calendar';
  915. var defaultMsgOpenCalendar = 'Open calendar';
  916. var service = {
  917. months: this.months || $locale.DATETIME_FORMATS.MONTH,
  918. shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
  919. days: this.days || $locale.DATETIME_FORMATS.DAY,
  920. shortDays: this.shortDays || defaultShortDays,
  921. dates: this.dates || defaultDates,
  922. firstDayOfWeek: this.firstDayOfWeek || 0,
  923. formatDate: this.formatDate || defaultFormatDate,
  924. parseDate: this.parseDate || defaultParseDate,
  925. isDateComplete: this.isDateComplete || defaultIsDateComplete,
  926. monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter,
  927. weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter,
  928. longDateFormatter: this.longDateFormatter || defaultLongDateFormatter,
  929. msgCalendar: this.msgCalendar || defaultMsgCalendar,
  930. msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar
  931. };
  932. return service;
  933. };
  934. DateLocaleProvider.prototype.$get.$inject = ["$locale"];
  935. $provide.provider('$mdDateLocale', new DateLocaleProvider());
  936. }]);
  937. })();
  938. (function() {
  939. 'use strict';
  940. // POST RELEASE
  941. // TODO(jelbourn): Demo that uses moment.js
  942. // TODO(jelbourn): make sure this plays well with validation and ngMessages.
  943. // TODO(jelbourn): calendar pane doesn't open up outside of visible viewport.
  944. // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.)
  945. // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?)
  946. // TODO(jelbourn): input behavior (masking? auto-complete?)
  947. // TODO(jelbourn): UTC mode
  948. // TODO(jelbourn): RTL
  949. angular.module('material.components.datepicker')
  950. .directive('mdDatepicker', datePickerDirective);
  951. /**
  952. * @ngdoc directive
  953. * @name mdDatepicker
  954. * @module material.components.datepicker
  955. *
  956. * @param {Date} ng-model The component's model. Expects a JavaScript Date object.
  957. * @param {expression=} ng-change Expression evaluated when the model value changes.
  958. * @param {Date=} md-min-date Expression representing a min date (inclusive).
  959. * @param {Date=} md-max-date Expression representing a max date (inclusive).
  960. * @param {boolean=} disabled Whether the datepicker is disabled.
  961. * @param {boolean=} required Whether a value is required for the datepicker.
  962. *
  963. * @description
  964. * `<md-datepicker>` is a component used to select a single date.
  965. * For information on how to configure internationalization for the date picker,
  966. * see `$mdDateLocaleProvider`.
  967. *
  968. * This component supports [ngMessages](https://docs.angularjs.org/api/ngMessages/directive/ngMessages).
  969. * Supported attributes are:
  970. * * `required`: whether a required date is not set.
  971. * * `mindate`: whether the selected date is before the minimum allowed date.
  972. * * `maxdate`: whether the selected date is after the maximum allowed date.
  973. *
  974. * @usage
  975. * <hljs lang="html">
  976. * <md-datepicker ng-model="birthday"></md-datepicker>
  977. * </hljs>
  978. *
  979. */
  980. function datePickerDirective() {
  981. return {
  982. template:
  983. // Buttons are not in the tab order because users can open the calendar via keyboard
  984. // interaction on the text input, and multiple tab stops for one component (picker)
  985. // may be confusing.
  986. '<md-button class="md-datepicker-button md-icon-button" type="button" ' +
  987. 'tabindex="-1" aria-hidden="true" ' +
  988. 'ng-click="ctrl.openCalendarPane($event)">' +
  989. '<md-icon class="md-datepicker-calendar-icon" md-svg-icon="md-calendar"></md-icon>' +
  990. '</md-button>' +
  991. '<div class="md-datepicker-input-container" ' +
  992. 'ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' +
  993. '<input class="md-datepicker-input" aria-haspopup="true" ' +
  994. 'ng-focus="ctrl.setFocused(true)" ng-blur="ctrl.setFocused(false)">' +
  995. '<md-button type="button" md-no-ink ' +
  996. 'class="md-datepicker-triangle-button md-icon-button" ' +
  997. 'ng-click="ctrl.openCalendarPane($event)" ' +
  998. 'aria-label="{{::ctrl.dateLocale.msgOpenCalendar}}">' +
  999. '<div class="md-datepicker-expand-triangle"></div>' +
  1000. '</md-button>' +
  1001. '</div>' +
  1002. // This pane will be detached from here and re-attached to the document body.
  1003. '<div class="md-datepicker-calendar-pane md-whiteframe-z1">' +
  1004. '<div class="md-datepicker-input-mask">' +
  1005. '<div class="md-datepicker-input-mask-opaque"></div>' +
  1006. '</div>' +
  1007. '<div class="md-datepicker-calendar">' +
  1008. '<md-calendar role="dialog" aria-label="{{::ctrl.dateLocale.msgCalendar}}" ' +
  1009. 'md-min-date="ctrl.minDate" md-max-date="ctrl.maxDate"' +
  1010. 'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' +
  1011. '</md-calendar>' +
  1012. '</div>' +
  1013. '</div>',
  1014. require: ['ngModel', 'mdDatepicker'],
  1015. scope: {
  1016. minDate: '=mdMinDate',
  1017. maxDate: '=mdMaxDate',
  1018. placeholder: '@mdPlaceholder'
  1019. },
  1020. controller: DatePickerCtrl,
  1021. controllerAs: 'ctrl',
  1022. bindToController: true,
  1023. link: function(scope, element, attr, controllers) {
  1024. var ngModelCtrl = controllers[0];
  1025. var mdDatePickerCtrl = controllers[1];
  1026. mdDatePickerCtrl.configureNgModel(ngModelCtrl);
  1027. }
  1028. };
  1029. }
  1030. /** Additional offset for the input's `size` attribute, which is updated based on its content. */
  1031. var EXTRA_INPUT_SIZE = 3;
  1032. /** Class applied to the container if the date is invalid. */
  1033. var INVALID_CLASS = 'md-datepicker-invalid';
  1034. /** Default time in ms to debounce input event by. */
  1035. var DEFAULT_DEBOUNCE_INTERVAL = 500;
  1036. /**
  1037. * Height of the calendar pane used to check if the pane is going outside the boundary of
  1038. * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is
  1039. * also added to space the pane away from the exact edge of the screen.
  1040. *
  1041. * This is computed statically now, but can be changed to be measured if the circumstances
  1042. * of calendar sizing are changed.
  1043. */
  1044. var CALENDAR_PANE_HEIGHT = 368;
  1045. /**
  1046. * Width of the calendar pane used to check if the pane is going outside the boundary of
  1047. * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is
  1048. * also added to space the pane away from the exact edge of the screen.
  1049. *
  1050. * This is computed statically now, but can be changed to be measured if the circumstances
  1051. * of calendar sizing are changed.
  1052. */
  1053. var CALENDAR_PANE_WIDTH = 360;
  1054. /**
  1055. * Controller for md-datepicker.
  1056. *
  1057. * ngInject @constructor
  1058. */
  1059. function DatePickerCtrl($scope, $element, $attrs, $compile, $timeout, $window,
  1060. $mdConstant, $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) {
  1061. /** @final */
  1062. this.$compile = $compile;
  1063. /** @final */
  1064. this.$timeout = $timeout;
  1065. /** @final */
  1066. this.$window = $window;
  1067. /** @final */
  1068. this.dateLocale = $mdDateLocale;
  1069. /** @final */
  1070. this.dateUtil = $$mdDateUtil;
  1071. /** @final */
  1072. this.$mdConstant = $mdConstant;
  1073. /* @final */
  1074. this.$mdUtil = $mdUtil;
  1075. /** @final */
  1076. this.$$rAF = $$rAF;
  1077. /** @type {!angular.NgModelController} */
  1078. this.ngModelCtrl = null;
  1079. /** @type {HTMLInputElement} */
  1080. this.inputElement = $element[0].querySelector('input');
  1081. /** @final {!angular.JQLite} */
  1082. this.ngInputElement = angular.element(this.inputElement);
  1083. /** @type {HTMLElement} */
  1084. this.inputContainer = $element[0].querySelector('.md-datepicker-input-container');
  1085. /** @type {HTMLElement} Floating calendar pane. */
  1086. this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane');
  1087. /** @type {HTMLElement} Calendar icon button. */
  1088. this.calendarButton = $element[0].querySelector('.md-datepicker-button');
  1089. /**
  1090. * Element covering everything but the input in the top of the floating calendar pane.
  1091. * @type {HTMLElement}
  1092. */
  1093. this.inputMask = $element[0].querySelector('.md-datepicker-input-mask-opaque');
  1094. /** @final {!angular.JQLite} */
  1095. this.$element = $element;
  1096. /** @final {!angular.Attributes} */
  1097. this.$attrs = $attrs;
  1098. /** @final {!angular.Scope} */
  1099. this.$scope = $scope;
  1100. /** @type {Date} */
  1101. this.date = null;
  1102. /** @type {boolean} */
  1103. this.isFocused = false;
  1104. /** @type {boolean} */
  1105. this.isDisabled;
  1106. this.setDisabled($element[0].disabled || angular.isString($attrs['disabled']));
  1107. /** @type {boolean} Whether the date-picker's calendar pane is open. */
  1108. this.isCalendarOpen = false;
  1109. /**
  1110. * Element from which the calendar pane was opened. Keep track of this so that we can return
  1111. * focus to it when the pane is closed.
  1112. * @type {HTMLElement}
  1113. */
  1114. this.calendarPaneOpenedFrom = null;
  1115. this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid();
  1116. $mdTheming($element);
  1117. /** Pre-bound click handler is saved so that the event listener can be removed. */
  1118. this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
  1119. /** Pre-bound resize handler so that the event listener can be removed. */
  1120. this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100);
  1121. // Unless the user specifies so, the datepicker should not be a tab stop.
  1122. // This is necessary because ngAria might add a tabindex to anything with an ng-model
  1123. // (based on whether or not the user has turned that particular feature on/off).
  1124. if (!$attrs['tabindex']) {
  1125. $element.attr('tabindex', '-1');
  1126. }
  1127. this.installPropertyInterceptors();
  1128. this.attachChangeListeners();
  1129. this.attachInteractionListeners();
  1130. var self = this;
  1131. $scope.$on('$destroy', function() {
  1132. self.detachCalendarPane();
  1133. });
  1134. }
  1135. DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"];
  1136. /**
  1137. * Sets up the controller's reference to ngModelController.
  1138. * @param {!angular.NgModelController} ngModelCtrl
  1139. */
  1140. DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) {
  1141. this.ngModelCtrl = ngModelCtrl;
  1142. var self = this;
  1143. ngModelCtrl.$render = function() {
  1144. self.date = self.ngModelCtrl.$viewValue;
  1145. self.inputElement.value = self.dateLocale.formatDate(self.date);
  1146. self.resizeInputElement();
  1147. self.setErrorFlags();
  1148. };
  1149. };
  1150. /**
  1151. * Attach event listeners for both the text input and the md-calendar.
  1152. * Events are used instead of ng-model so that updates don't infinitely update the other
  1153. * on a change. This should also be more performant than using a $watch.
  1154. */
  1155. DatePickerCtrl.prototype.attachChangeListeners = function() {
  1156. var self = this;
  1157. self.$scope.$on('md-calendar-change', function(event, date) {
  1158. self.ngModelCtrl.$setViewValue(date);
  1159. self.date = date;
  1160. self.inputElement.value = self.dateLocale.formatDate(date);
  1161. self.closeCalendarPane();
  1162. self.resizeInputElement();
  1163. self.inputContainer.classList.remove(INVALID_CLASS);
  1164. });
  1165. self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement));
  1166. // TODO(chenmike): Add ability for users to specify this interval.
  1167. self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
  1168. DEFAULT_DEBOUNCE_INTERVAL, self));
  1169. };
  1170. /** Attach event listeners for user interaction. */
  1171. DatePickerCtrl.prototype.attachInteractionListeners = function() {
  1172. var self = this;
  1173. var $scope = this.$scope;
  1174. var keyCodes = this.$mdConstant.KEY_CODE;
  1175. // Add event listener through angular so that we can triggerHandler in unit tests.
  1176. self.ngInputElement.on('keydown', function(event) {
  1177. if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
  1178. self.openCalendarPane(event);
  1179. $scope.$digest();
  1180. }
  1181. });
  1182. $scope.$on('md-calendar-close', function() {
  1183. self.closeCalendarPane();
  1184. });
  1185. };
  1186. /**
  1187. * Capture properties set to the date-picker and imperitively handle internal changes.
  1188. * This is done to avoid setting up additional $watches.
  1189. */
  1190. DatePickerCtrl.prototype.installPropertyInterceptors = function() {
  1191. var self = this;
  1192. if (this.$attrs['ngDisabled']) {
  1193. // The expression is to be evaluated against the directive element's scope and not
  1194. // the directive's isolate scope.
  1195. var scope = this.$mdUtil.validateScope(this.$element) ? this.$element.scope() : null;
  1196. if ( scope ) {
  1197. scope.$watch(this.$attrs['ngDisabled'], function(isDisabled) {
  1198. self.setDisabled(isDisabled);
  1199. });
  1200. }
  1201. }
  1202. Object.defineProperty(this, 'placeholder', {
  1203. get: function() { return self.inputElement.placeholder; },
  1204. set: function(value) { self.inputElement.placeholder = value || ''; }
  1205. });
  1206. };
  1207. /**
  1208. * Sets whether the date-picker is disabled.
  1209. * @param {boolean} isDisabled
  1210. */
  1211. DatePickerCtrl.prototype.setDisabled = function(isDisabled) {
  1212. this.isDisabled = isDisabled;
  1213. this.inputElement.disabled = isDisabled;
  1214. this.calendarButton.disabled = isDisabled;
  1215. };
  1216. /**
  1217. * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are:
  1218. * - mindate: whether the selected date is before the minimum date.
  1219. * - maxdate: whether the selected flag is after the maximum date.
  1220. */
  1221. DatePickerCtrl.prototype.setErrorFlags = function() {
  1222. if (this.dateUtil.isValidDate(this.date)) {
  1223. if (this.dateUtil.isValidDate(this.minDate)) {
  1224. this.ngModelCtrl.$error['mindate'] = this.date < this.minDate;
  1225. }
  1226. if (this.dateUtil.isValidDate(this.maxDate)) {
  1227. this.ngModelCtrl.$error['maxdate'] = this.date > this.maxDate;
  1228. }
  1229. }
  1230. };
  1231. /** Resizes the input element based on the size of its content. */
  1232. DatePickerCtrl.prototype.resizeInputElement = function() {
  1233. this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE;
  1234. };
  1235. /**
  1236. * Sets the model value if the user input is a valid date.
  1237. * Adds an invalid class to the input element if not.
  1238. */
  1239. DatePickerCtrl.prototype.handleInputEvent = function() {
  1240. var inputString = this.inputElement.value;
  1241. var parsedDate = this.dateLocale.parseDate(inputString);
  1242. this.dateUtil.setDateTimeToMidnight(parsedDate);
  1243. if (inputString === '') {
  1244. this.ngModelCtrl.$setViewValue(null);
  1245. this.date = null;
  1246. this.inputContainer.classList.remove(INVALID_CLASS);
  1247. } else if (this.dateUtil.isValidDate(parsedDate) &&
  1248. this.dateLocale.isDateComplete(inputString) &&
  1249. this.dateUtil.isDateWithinRange(parsedDate, this.minDate, this.maxDate)) {
  1250. this.ngModelCtrl.$setViewValue(parsedDate);
  1251. this.date = parsedDate;
  1252. this.inputContainer.classList.remove(INVALID_CLASS);
  1253. } else {
  1254. // If there's an input string, it's an invalid date.
  1255. this.inputContainer.classList.toggle(INVALID_CLASS, inputString);
  1256. }
  1257. };
  1258. /** Position and attach the floating calendar to the document. */
  1259. DatePickerCtrl.prototype.attachCalendarPane = function() {
  1260. var calendarPane = this.calendarPane;
  1261. calendarPane.style.transform = '';
  1262. this.$element.addClass('md-datepicker-open');
  1263. var elementRect = this.inputContainer.getBoundingClientRect();
  1264. var bodyRect = document.body.getBoundingClientRect();
  1265. // Check to see if the calendar pane would go off the screen. If so, adjust position
  1266. // accordingly to keep it within the viewport.
  1267. var paneTop = elementRect.top - bodyRect.top;
  1268. var paneLeft = elementRect.left - bodyRect.left;
  1269. var viewportTop = document.body.scrollTop;
  1270. var viewportBottom = viewportTop + this.$window.innerHeight;
  1271. var viewportLeft = document.body.scrollLeft;
  1272. var viewportRight = document.body.scrollLeft + this.$window.innerWidth;
  1273. // If the right edge of the pane would be off the screen and shifting it left by the
  1274. // difference would not go past the left edge of the screen. If the calendar pane is too
  1275. // big to fit on the screen at all, move it to the left of the screen and scale the entire
  1276. // element down to fit.
  1277. if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) {
  1278. if (viewportRight - CALENDAR_PANE_WIDTH > 0) {
  1279. paneLeft = viewportRight - CALENDAR_PANE_WIDTH;
  1280. } else {
  1281. paneLeft = viewportLeft;
  1282. var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH;
  1283. calendarPane.style.transform = 'scale(' + scale + ')';
  1284. }
  1285. calendarPane.classList.add('md-datepicker-pos-adjusted');
  1286. }
  1287. // If the bottom edge of the pane would be off the screen and shifting it up by the
  1288. // difference would not go past the top edge of the screen.
  1289. if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom &&
  1290. viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) {
  1291. paneTop = viewportBottom - CALENDAR_PANE_HEIGHT;
  1292. calendarPane.classList.add('md-datepicker-pos-adjusted');
  1293. }
  1294. calendarPane.style.left = paneLeft + 'px';
  1295. calendarPane.style.top = paneTop + 'px';
  1296. document.body.appendChild(calendarPane);
  1297. // The top of the calendar pane is a transparent box that shows the text input underneath.
  1298. // Since the pane is floating, though, the page underneath the pane *adjacent* to the input is
  1299. // also shown unless we cover it up. The inputMask does this by filling up the remaining space
  1300. // based on the width of the input.
  1301. this.inputMask.style.left = elementRect.width + 'px';
  1302. // Add CSS class after one frame to trigger open animation.
  1303. this.$$rAF(function() {
  1304. calendarPane.classList.add('md-pane-open');
  1305. });
  1306. };
  1307. /** Detach the floating calendar pane from the document. */
  1308. DatePickerCtrl.prototype.detachCalendarPane = function() {
  1309. this.$element.removeClass('md-datepicker-open');
  1310. this.calendarPane.classList.remove('md-pane-open');
  1311. this.calendarPane.classList.remove('md-datepicker-pos-adjusted');
  1312. if (this.calendarPane.parentNode) {
  1313. // Use native DOM removal because we do not want any of the angular state of this element
  1314. // to be disposed.
  1315. this.calendarPane.parentNode.removeChild(this.calendarPane);
  1316. }
  1317. };
  1318. /**
  1319. * Open the floating calendar pane.
  1320. * @param {Event} event
  1321. */
  1322. DatePickerCtrl.prototype.openCalendarPane = function(event) {
  1323. if (!this.isCalendarOpen && !this.isDisabled) {
  1324. this.isCalendarOpen = true;
  1325. this.calendarPaneOpenedFrom = event.target;
  1326. this.attachCalendarPane();
  1327. this.focusCalendar();
  1328. // Because the calendar pane is attached directly to the body, it is possible that the
  1329. // rest of the component (input, etc) is in a different scrolling container, such as
  1330. // an md-content. This means that, if the container is scrolled, the pane would remain
  1331. // stationary. To remedy this, we disable scrolling while the calendar pane is open, which
  1332. // also matches the native behavior for things like `<select>` on Mac and Windows.
  1333. this.$mdUtil.disableScrollAround(this.calendarPane);
  1334. // Attach click listener inside of a timeout because, if this open call was triggered by a
  1335. // click, we don't want it to be immediately propogated up to the body and handled.
  1336. var self = this;
  1337. this.$mdUtil.nextTick(function() {
  1338. document.body.addEventListener('click', self.bodyClickHandler);
  1339. }, false);
  1340. window.addEventListener('resize', this.windowResizeHandler);
  1341. }
  1342. };
  1343. /** Close the floating calendar pane. */
  1344. DatePickerCtrl.prototype.closeCalendarPane = function() {
  1345. if (this.isCalendarOpen) {
  1346. this.isCalendarOpen = false;
  1347. this.detachCalendarPane();
  1348. this.calendarPaneOpenedFrom.focus();
  1349. this.calendarPaneOpenedFrom = null;
  1350. this.$mdUtil.enableScrolling();
  1351. document.body.removeEventListener('click', this.bodyClickHandler);
  1352. window.removeEventListener('resize', this.windowResizeHandler);
  1353. }
  1354. };
  1355. /** Gets the controller instance for the calendar in the floating pane. */
  1356. DatePickerCtrl.prototype.getCalendarCtrl = function() {
  1357. return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
  1358. };
  1359. /** Focus the calendar in the floating pane. */
  1360. DatePickerCtrl.prototype.focusCalendar = function() {
  1361. // Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
  1362. var self = this;
  1363. this.$mdUtil.nextTick(function() {
  1364. self.getCalendarCtrl().focus();
  1365. }, false);
  1366. };
  1367. /**
  1368. * Sets whether the input is currently focused.
  1369. * @param {boolean} isFocused
  1370. */
  1371. DatePickerCtrl.prototype.setFocused = function(isFocused) {
  1372. this.isFocused = isFocused;
  1373. };
  1374. /**
  1375. * Handles a click on the document body when the floating calendar pane is open.
  1376. * Closes the floating calendar pane if the click is not inside of it.
  1377. * @param {MouseEvent} event
  1378. */
  1379. DatePickerCtrl.prototype.handleBodyClick = function(event) {
  1380. if (this.isCalendarOpen) {
  1381. // TODO(jelbourn): way want to also include the md-datepicker itself in this check.
  1382. var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
  1383. if (!isInCalendar) {
  1384. this.closeCalendarPane();
  1385. }
  1386. this.$scope.$digest();
  1387. }
  1388. };
  1389. })();
  1390. (function() {
  1391. 'use strict';
  1392. /**
  1393. * Utility for performing date calculations to facilitate operation of the calendar and
  1394. * datepicker.
  1395. */
  1396. angular.module('material.components.datepicker').factory('$$mdDateUtil', function() {
  1397. return {
  1398. getFirstDateOfMonth: getFirstDateOfMonth,
  1399. getNumberOfDaysInMonth: getNumberOfDaysInMonth,
  1400. getDateInNextMonth: getDateInNextMonth,
  1401. getDateInPreviousMonth: getDateInPreviousMonth,
  1402. isInNextMonth: isInNextMonth,
  1403. isInPreviousMonth: isInPreviousMonth,
  1404. getDateMidpoint: getDateMidpoint,
  1405. isSameMonthAndYear: isSameMonthAndYear,
  1406. getWeekOfMonth: getWeekOfMonth,
  1407. incrementDays: incrementDays,
  1408. incrementMonths: incrementMonths,
  1409. getLastDateOfMonth: getLastDateOfMonth,
  1410. isSameDay: isSameDay,
  1411. getMonthDistance: getMonthDistance,
  1412. isValidDate: isValidDate,
  1413. setDateTimeToMidnight: setDateTimeToMidnight,
  1414. createDateAtMidnight: createDateAtMidnight,
  1415. isDateWithinRange: isDateWithinRange
  1416. };
  1417. /**
  1418. * Gets the first day of the month for the given date's month.
  1419. * @param {Date} date
  1420. * @returns {Date}
  1421. */
  1422. function getFirstDateOfMonth(date) {
  1423. return new Date(date.getFullYear(), date.getMonth(), 1);
  1424. }
  1425. /**
  1426. * Gets the number of days in the month for the given date's month.
  1427. * @param date
  1428. * @returns {number}
  1429. */
  1430. function getNumberOfDaysInMonth(date) {
  1431. return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
  1432. }
  1433. /**
  1434. * Get an arbitrary date in the month after the given date's month.
  1435. * @param date
  1436. * @returns {Date}
  1437. */
  1438. function getDateInNextMonth(date) {
  1439. return new Date(date.getFullYear(), date.getMonth() + 1, 1);
  1440. }
  1441. /**
  1442. * Get an arbitrary date in the month before the given date's month.
  1443. * @param date
  1444. * @returns {Date}
  1445. */
  1446. function getDateInPreviousMonth(date) {
  1447. return new Date(date.getFullYear(), date.getMonth() - 1, 1);
  1448. }
  1449. /**
  1450. * Gets whether two dates have the same month and year.
  1451. * @param {Date} d1
  1452. * @param {Date} d2
  1453. * @returns {boolean}
  1454. */
  1455. function isSameMonthAndYear(d1, d2) {
  1456. return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
  1457. }
  1458. /**
  1459. * Gets whether two dates are the same day (not not necesarily the same time).
  1460. * @param {Date} d1
  1461. * @param {Date} d2
  1462. * @returns {boolean}
  1463. */
  1464. function isSameDay(d1, d2) {
  1465. return d1.getDate() == d2.getDate() && isSameMonthAndYear(d1, d2);
  1466. }
  1467. /**
  1468. * Gets whether a date is in the month immediately after some date.
  1469. * @param {Date} startDate The date from which to compare.
  1470. * @param {Date} endDate The date to check.
  1471. * @returns {boolean}
  1472. */
  1473. function isInNextMonth(startDate, endDate) {
  1474. var nextMonth = getDateInNextMonth(startDate);
  1475. return isSameMonthAndYear(nextMonth, endDate);
  1476. }
  1477. /**
  1478. * Gets whether a date is in the month immediately before some date.
  1479. * @param {Date} startDate The date from which to compare.
  1480. * @param {Date} endDate The date to check.
  1481. * @returns {boolean}
  1482. */
  1483. function isInPreviousMonth(startDate, endDate) {
  1484. var previousMonth = getDateInPreviousMonth(startDate);
  1485. return isSameMonthAndYear(endDate, previousMonth);
  1486. }
  1487. /**
  1488. * Gets the midpoint between two dates.
  1489. * @param {Date} d1
  1490. * @param {Date} d2
  1491. * @returns {Date}
  1492. */
  1493. function getDateMidpoint(d1, d2) {
  1494. return createDateAtMidnight((d1.getTime() + d2.getTime()) / 2);
  1495. }
  1496. /**
  1497. * Gets the week of the month that a given date occurs in.
  1498. * @param {Date} date
  1499. * @returns {number} Index of the week of the month (zero-based).
  1500. */
  1501. function getWeekOfMonth(date) {
  1502. var firstDayOfMonth = getFirstDateOfMonth(date);
  1503. return Math.floor((firstDayOfMonth.getDay() + date.getDate() - 1) / 7);
  1504. }
  1505. /**
  1506. * Gets a new date incremented by the given number of days. Number of days can be negative.
  1507. * @param {Date} date
  1508. * @param {number} numberOfDays
  1509. * @returns {Date}
  1510. */
  1511. function incrementDays(date, numberOfDays) {
  1512. return new Date(date.getFullYear(), date.getMonth(), date.getDate() + numberOfDays);
  1513. }
  1514. /**
  1515. * Gets a new date incremented by the given number of months. Number of months can be negative.
  1516. * If the date of the given month does not match the target month, the date will be set to the
  1517. * last day of the month.
  1518. * @param {Date} date
  1519. * @param {number} numberOfMonths
  1520. * @returns {Date}
  1521. */
  1522. function incrementMonths(date, numberOfMonths) {
  1523. // If the same date in the target month does not actually exist, the Date object will
  1524. // automatically advance *another* month by the number of missing days.
  1525. // For example, if you try to go from Jan. 30 to Feb. 30, you'll end up on March 2.
  1526. // So, we check if the month overflowed and go to the last day of the target month instead.
  1527. var dateInTargetMonth = new Date(date.getFullYear(), date.getMonth() + numberOfMonths, 1);
  1528. var numberOfDaysInMonth = getNumberOfDaysInMonth(dateInTargetMonth);
  1529. if (numberOfDaysInMonth < date.getDate()) {
  1530. dateInTargetMonth.setDate(numberOfDaysInMonth);
  1531. } else {
  1532. dateInTargetMonth.setDate(date.getDate());
  1533. }
  1534. return dateInTargetMonth;
  1535. }
  1536. /**
  1537. * Get the integer distance between two months. This *only* considers the month and year
  1538. * portion of the Date instances.
  1539. *
  1540. * @param {Date} start
  1541. * @param {Date} end
  1542. * @returns {number} Number of months between `start` and `end`. If `end` is before `start`
  1543. * chronologically, this number will be negative.
  1544. */
  1545. function getMonthDistance(start, end) {
  1546. return (12 * (end.getFullYear() - start.getFullYear())) + (end.getMonth() - start.getMonth());
  1547. }
  1548. /**
  1549. * Gets the last day of the month for the given date.
  1550. * @param {Date} date
  1551. * @returns {Date}
  1552. */
  1553. function getLastDateOfMonth(date) {
  1554. return new Date(date.getFullYear(), date.getMonth(), getNumberOfDaysInMonth(date));
  1555. }
  1556. /**
  1557. * Checks whether a date is valid.
  1558. * @param {Date} date
  1559. * @return {boolean} Whether the date is a valid Date.
  1560. */
  1561. function isValidDate(date) {
  1562. return date != null && date.getTime && !isNaN(date.getTime());
  1563. }
  1564. /**
  1565. * Sets a date's time to midnight.
  1566. * @param {Date} date
  1567. */
  1568. function setDateTimeToMidnight(date) {
  1569. if (isValidDate(date)) {
  1570. date.setHours(0, 0, 0, 0);
  1571. }
  1572. }
  1573. /**
  1574. * Creates a date with the time set to midnight.
  1575. * Drop-in replacement for two forms of the Date constructor:
  1576. * 1. No argument for Date representing now.
  1577. * 2. Single-argument value representing number of seconds since Unix Epoch.
  1578. * @param {number=} opt_value
  1579. * @return {Date} New date with time set to midnight.
  1580. */
  1581. function createDateAtMidnight(opt_value) {
  1582. var date;
  1583. if (angular.isUndefined(opt_value)) {
  1584. date = new Date();
  1585. } else {
  1586. date = new Date(opt_value);
  1587. }
  1588. setDateTimeToMidnight(date);
  1589. return date;
  1590. }
  1591. /**
  1592. * Checks if a date is within a min and max range.
  1593. * If minDate or maxDate are not dates, they are ignored.
  1594. * @param {Date} date
  1595. * @param {Date} minDate
  1596. * @param {Date} maxDate
  1597. */
  1598. function isDateWithinRange(date, minDate, maxDate) {
  1599. return (!angular.isDate(minDate) || minDate <= date) &&
  1600. (!angular.isDate(maxDate) || maxDate >= date);
  1601. }
  1602. });
  1603. })();
  1604. ng.material.components.datepicker = angular.module("material.components.datepicker");