datepicker.js 65 KB

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