From b0fdf73890873be2b440324c192bd3bef4883cd9 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sun, 28 Jun 2020 18:20:40 -0400 Subject: [PATCH] fix(datepicker): support ng-model-options timezone w/ Moment - fix case where datepicker's model is initially out of sync with the input value - add demo for `ng-model-options` timezone support - pass datepicker's `ng-model-options` on to its calendar Fixes #11945. Fixes #10598. --- package-lock.json | 2 +- package.json | 2 +- .../demoNgModelOptionsTimezone/index.html | 32 +++++++++++++++++++ .../demoNgModelOptionsTimezone/script.js | 8 +++++ src/components/datepicker/js/calendar.js | 11 +++---- src/components/datepicker/js/calendarMonth.js | 3 +- src/components/datepicker/js/calendarYear.js | 3 +- src/components/datepicker/js/dateUtil.js | 18 +++++++++-- .../datepicker/js/datepickerDirective.js | 28 +++++++++++----- src/core/util/util.js | 6 ++-- 10 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 src/components/datepicker/demoNgModelOptionsTimezone/index.html create mode 100644 src/components/datepicker/demoNgModelOptionsTimezone/script.js diff --git a/package-lock.json b/package-lock.json index 32c634d381f..3de1baac556 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "angular-material-source", - "version": "1.1.22", + "version": "1.1.23", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index bfd7c6b2bb9..5d8bf07b8e0 100644 --- a/package.json +++ b/package.json @@ -132,4 +132,4 @@ "node": ">=10", "npm": ">=6" } -} \ No newline at end of file +} diff --git a/src/components/datepicker/demoNgModelOptionsTimezone/index.html b/src/components/datepicker/demoNgModelOptionsTimezone/index.html new file mode 100644 index 00000000000..a800537de96 --- /dev/null +++ b/src/components/datepicker/demoNgModelOptionsTimezone/index.html @@ -0,0 +1,32 @@ + + + + +
+
+

Calendar Values

+
+ Date in local timezone: + {{ctrl.calendarDate|date:"yyyy-MM-dd HH:mm Z"}} +
+
+ Date in UTC timezone: + {{ctrl.calendarDate|date:"yyyy-MM-dd HH:mm Z":"UTC"}} +
+
+ + + +
+

Datepicker Values

+
+ Date in local timezone: + {{ctrl.datepickerDate|date:"yyyy-MM-dd HH:mm Z"}} +
+
+ Date in UTC timezone: + {{ctrl.datepickerDate|date:"yyyy-MM-dd HH:mm Z":"UTC"}} +
+
+
+
diff --git a/src/components/datepicker/demoNgModelOptionsTimezone/script.js b/src/components/datepicker/demoNgModelOptionsTimezone/script.js new file mode 100644 index 00000000000..c5f736800e9 --- /dev/null +++ b/src/components/datepicker/demoNgModelOptionsTimezone/script.js @@ -0,0 +1,8 @@ +angular.module('ngModelTimezoneUsage', ['ngMaterial', 'ngMessages']) +.controller('AppCtrl', function() { + this.datepickerDate = new Date(0); + this.datepickerDate.setUTCFullYear(2020, 5, 19); + + this.calendarDate = new Date(0); + this.calendarDate.setUTCFullYear(2020, 5, 19); +}); diff --git a/src/components/datepicker/js/calendar.js b/src/components/datepicker/js/calendar.js index 7458e7c16ea..e2423e4274b 100644 --- a/src/components/datepicker/js/calendar.js +++ b/src/components/datepicker/js/calendar.js @@ -321,17 +321,14 @@ }, this.$attrs, [ngModelCtrl]); ngModelCtrl.$render = function() { - var value = this.$viewValue; - var parsedValue, convertedValue; + var value = this.$viewValue, convertedDate; // In the case where a conversion is needed, the $viewValue here will be a string like // "2020-05-10" instead of a Date object. if (!self.dateUtil.isValidDate(value)) { - parsedValue = self.$mdDateLocale.parseDate(this.$viewValue); - convertedValue = - new Date(parsedValue.getTime() + 60000 * parsedValue.getTimezoneOffset()); - if (self.dateUtil.isValidDate(convertedValue)) { - value = convertedValue; + convertedDate = self.dateUtil.removeLocalTzAndReparseDate(new Date(this.$viewValue)); + if (self.dateUtil.isValidDate(convertedDate)) { + value = convertedDate; } } diff --git a/src/components/datepicker/js/calendarMonth.js b/src/components/datepicker/js/calendarMonth.js index 50c9332bb7b..5c10c38c5fe 100644 --- a/src/components/datepicker/js/calendarMonth.js +++ b/src/components/datepicker/js/calendarMonth.js @@ -98,7 +98,8 @@ this.cellClickHandler = function() { var timestamp = $$mdDateUtil.getTimestampFromNode(this); self.$scope.$apply(function() { - self.calendarCtrl.setNgModelValue(self.dateLocale.parseDate(timestamp)); + // The timestamp has to be converted to a valid date. + self.calendarCtrl.setNgModelValue(new Date(timestamp)); }); }; diff --git a/src/components/datepicker/js/calendarYear.js b/src/components/datepicker/js/calendarYear.js index 0ea22e7be52..b3ae054f109 100644 --- a/src/components/datepicker/js/calendarYear.js +++ b/src/components/datepicker/js/calendarYear.js @@ -227,7 +227,8 @@ if (calendarCtrl.mode) { this.$mdUtil.nextTick(function() { - calendarCtrl.setNgModelValue(calendarCtrl.$mdDateLocale.parseDate(timestamp)); + // The timestamp has to be converted to a valid date. + calendarCtrl.setNgModelValue(new Date(timestamp)); }); } else { calendarCtrl.setCurrentView('month', timestamp); diff --git a/src/components/datepicker/js/dateUtil.js b/src/components/datepicker/js/dateUtil.js index 582e20a9127..ef42a0f3fb5 100644 --- a/src/components/datepicker/js/dateUtil.js +++ b/src/components/datepicker/js/dateUtil.js @@ -5,7 +5,7 @@ * Utility for performing date calculations to facilitate operation of the calendar and * datepicker. */ - angular.module('material.components.datepicker').factory('$$mdDateUtil', function() { + angular.module('material.components.datepicker').factory('$$mdDateUtil', function($mdDateLocale) { return { getFirstDateOfMonth: getFirstDateOfMonth, getNumberOfDaysInMonth: getNumberOfDaysInMonth, @@ -29,7 +29,8 @@ getYearDistance: getYearDistance, clampDate: clampDate, getTimestampFromNode: getTimestampFromNode, - isMonthWithinRange: isMonthWithinRange + isMonthWithinRange: isMonthWithinRange, + removeLocalTzAndReparseDate: removeLocalTzAndReparseDate }; /** @@ -307,5 +308,18 @@ return (!minDate || minDate.getFullYear() < year || minDate.getMonth() <= month) && (!maxDate || maxDate.getFullYear() > year || maxDate.getMonth() >= month); } + + /** + * @param {Date} value + * @return {boolean|boolean} + */ + function removeLocalTzAndReparseDate(value) { + var dateValue, formattedDate; + // Remove the local timezone offset before calling formatDate. + dateValue = new Date(value.getTime() + 60000 * value.getTimezoneOffset()); + formattedDate = $mdDateLocale.formatDate(dateValue); + // parseDate only works with a date formatted by formatDate when using Moment validation. + return $mdDateLocale.parseDate(formattedDate); + } }); })(); diff --git a/src/components/datepicker/js/datepickerDirective.js b/src/components/datepicker/js/datepickerDirective.js index 4cc9c19bd31..99efc62406f 100644 --- a/src/components/datepicker/js/datepickerDirective.js +++ b/src/components/datepicker/js/datepickerDirective.js @@ -85,6 +85,7 @@ // may be confusing. var hiddenIcons = tAttrs.mdHideIcons; var ariaLabelValue = tAttrs.ariaLabel || tAttrs.mdPlaceholder; + var ngModelOptions = tAttrs.ngModelOptions; var calendarButton = (hiddenIcons === 'all' || hiddenIcons === 'calendar') ? '' : '' + '' + '' + @@ -180,7 +182,8 @@ mdInputContainer.input = element; mdInputContainer.element .addClass(INPUT_CONTAINER_CLASS) - .toggleClass(HAS_CALENDAR_ICON_CLASS, attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all'); + .toggleClass(HAS_CALENDAR_ICON_CLASS, + attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all'); if (!mdInputContainer.label) { $mdAria.expect(element, 'aria-label', attr.mdPlaceholder); @@ -191,7 +194,8 @@ } scope.$watch(mdInputContainer.isErrorGetter || function() { - return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted)); + return ngModelCtrl.$invalid && (ngModelCtrl.$touched || + (parentForm && parentForm.$submitted)); }, mdInputContainer.setInvalid); } else if (parentForm) { // If invalid, highlights the input when the parent form is submitted. @@ -424,8 +428,8 @@ }); } - // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned, - // manually call the $onInit hook. + // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are + // pre-assigned, manually call the $onInit hook. if (angular.version.major === 1 && angular.version.minor <= 4) { this.$onInit(); } @@ -433,7 +437,8 @@ /** * AngularJS Lifecycle hook for newer AngularJS versions. - * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook. + * Bindings are not guaranteed to have been assigned in the controller, but they are in the + * $onInit hook. */ DatePickerCtrl.prototype.$onInit = function() { @@ -442,7 +447,8 @@ * the user to override specific ones from the $mdDateLocale provider. * @type {!Object} */ - this.locale = this.dateLocale ? angular.extend({}, this.$mdDateLocale, this.dateLocale) : this.$mdDateLocale; + this.locale = this.dateLocale ? angular.extend({}, this.$mdDateLocale, this.dateLocale) + : this.$mdDateLocale; this.installPropertyInterceptors(); this.attachChangeListeners(); @@ -743,7 +749,9 @@ var bodyRect = body.getBoundingClientRect(); if (!this.topMargin || this.topMargin < 0) { - this.topMargin = (this.inputMask.parent().prop('clientHeight') - this.ngInputElement.prop('clientHeight')) / 2; + this.topMargin = + (this.inputMask.parent().prop('clientHeight') + - this.ngInputElement.prop('clientHeight')) / 2; } // Check to see if the calendar pane would go off the screen. If so, adjust position @@ -993,7 +1001,11 @@ var self = this; var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone'); - this.date = value; + if (this.dateUtil.isValidDate(value)) { + this.date = this.dateUtil.removeLocalTzAndReparseDate(value); + } else { + this.date = value; + } this.inputElement.value = this.locale.formatDate(value, timezone); this.mdInputContainer && this.mdInputContainer.setHasValue(!!value); this.resizeInputElement(); diff --git a/src/core/util/util.js b/src/core/util/util.js index b831cf9ca38..37087eeae50 100644 --- a/src/core/util/util.js +++ b/src/core/util/util.js @@ -84,7 +84,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in * which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c). * @param {!ngModel.NgModelController} ngModelCtrl * @param {!string} optionName - * @returns {Object|undefined} + * @returns {string|number|boolean|Object|undefined} */ getModelOption: function (ngModelCtrl, optionName) { if (!ngModelCtrl.$options) { @@ -93,8 +93,8 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in var $options = ngModelCtrl.$options; - // The newer versions of AngularJS introduced a `getOption function and made the option values no longer - // visible on the $options object. + // The newer versions of AngularJS introduced a getOption function and made the option values + // no longer visible on the $options object. return $options.getOption ? $options.getOption(optionName) : $options[optionName]; },