blob: afd2e039fed0948e16469f1c93dbbbc88072e8d8 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* 'set-time-dialog' handles a dialog to check and set system time. It can also
* include a timezone dropdown if timezoneId is provided.
*
* 'set-time-dialog' uses the system time to populate the controls initially and
* update them as the system time or timezone changes, and notifies Chrome
* when the user changes the time or timezone.
*/
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import 'chrome://resources/cr_elements/cr_page_host_style_css.m.js';
import 'chrome://resources/cr_elements/md_select_css.m.js';
import 'chrome://resources/cr_elements/shared_style_css.m.js';
import './strings.m.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {WebUIListenerBehavior} from 'chrome://resources/js/web_ui_listener_behavior.m.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {SetTimeBrowserProxy, SetTimeBrowserProxyImpl} from './set_time_browser_proxy.js';
/**
* @return {!Array<!{id: string, name: string, selected: Boolean}>} Items for
* the timezone select element.
*/
function getTimezoneItems() {
const currentTimezoneId =
/** @type {string} */ (loadTimeData.getValue('currentTimezoneId'));
const timezoneList =
/** @type {!Array} */ (loadTimeData.getValue('timezoneList'));
return timezoneList.map(
tz => ({id: tz[0], name: tz[1], selected: tz[0] === currentTimezoneId}));
}
/**
* Builds date and time strings suitable for the values of HTML date and
* time elements.
* @param {!Date} date The date object to represent.
* @return {!{date: string, time: string}} Date is an RFC 3339 formatted date
* and time is an HH:MM formatted time.
* @private
*/
function dateToHtmlValues(date) {
// Get the current time and subtract the timezone offset, so the
// JSON string is in local time.
const localDate = new Date(date);
localDate.setMinutes(date.getMinutes() - date.getTimezoneOffset());
return {
date: localDate.toISOString().slice(0, 10),
time: localDate.toISOString().slice(11, 16)
};
}
/**
* @return {string} Minimum date for the date picker in RFC 3339 format.
*/
function getMinDate() {
// Start with the build date because we can't trust the clock. The build time
// doesn't include a timezone, so subtract 1 day to get a safe minimum date.
let minDate = new Date(loadTimeData.getValue('buildTime'));
minDate.setDate(minDate.getDate() - 1);
// Make sure the ostensible date is in range.
const now = new Date();
if (now < minDate)
minDate = now;
// Convert to string for date input min attribute.
return dateToHtmlValues(minDate).date;
}
/**
* @return {string} Maximum date for the date picker in RFC 3339 format.
*/
function getMaxDate() {
// Set the max date to the build date plus 20 years.
let maxDate = new Date(loadTimeData.getValue('buildTime'));
maxDate.setFullYear(maxDate.getFullYear() + 20);
// Make sure the ostensible date is in range.
const now = new Date();
if (now > maxDate)
maxDate = now;
// Convert to string for date input max attribute.
return dateToHtmlValues(maxDate).date;
}
/**
* Gets the current time in a different timezone.
* @param {!string} timezoneId The timezone to be used to convert the time.
* @return {Date} The converted time.
*/
function getDateInTimezone(timezoneId) {
return new Date(new Date().toLocaleString('en-US', {timeZone: timezoneId}));
}
/**
* Gives the time difference between two timezones.
* @param {!string} firstTimezoneId The timezone on the left-hand size of the
* subtraction.
* @param {!string} secondsTimezoneId The timezone on the right-hand side of the
* subtraction.
* @return {number} Delta in milliseconds between the two timezones.
*/
function getTimezoneDelta(firstTimezoneId, secondsTimezoneId) {
return getDateInTimezone(firstTimezoneId) -
getDateInTimezone(secondsTimezoneId);
}
Polymer({
is: 'set-time-dialog',
_template: html`{__html_template__}`,
// Remove listeners on detach.
behaviors: [WebUIListenerBehavior],
properties: {
/**
* Items to populate the timezone select.
* @private
*/
timezoneItems_: {
type: Array,
readonly: true,
value: getTimezoneItems,
},
/**
* Whether the timezone select element is visible.
* @private
*/
isTimezoneVisible_: {
type: Boolean,
readonly: true,
value: () =>
/** @type {boolean} */ (loadTimeData.getValue('showTimezone')),
},
/**
* The minimum date allowed in the date picker.
* @private
*/
minDate_: {
type: String,
readonly: true,
value: getMinDate,
},
/**
* The maximum date allowed in the date picker.
* @private
*/
maxDate_: {
type: String,
readonly: true,
value: getMaxDate,
},
/**
* The last timezone selected.
* @private
*/
selectedTimezone_: {
type: String,
value: () =>
/** @type {string} */ (loadTimeData.getValue('currentTimezoneId')),
},
},
/**
* Values for reverting inputs when the user's date/time is invalid. The
* keys are element ids.
* @private {{dateInput: string, timeInput: string}}
*/
prevValues_: {dateInput: '', timeInput: ''},
/**
* ID of the setTimeout() used to refresh the current time.
* @private {?number}
*/
timeTimeoutId_: null,
/** @private {?SetTimeBrowserProxy} */
browserProxy_: null,
/** @override */
created() {
this.browserProxy_ = SetTimeBrowserProxyImpl.getInstance();
},
/** @override */
ready() {
this.updateTime_(new Date());
},
/** @override */
attached() {
// Register listeners for updates from C++ code.
this.addWebUIListener(
'system-clock-updated', this.updateTime_.bind(this, new Date()));
this.addWebUIListener(
'system-timezone-changed', this.setTimezone_.bind(this));
this.addWebUIListener('validation-complete', this.saveAndClose_.bind(this));
this.browserProxy_.sendPageReady();
/** @type {!CrDialogElement} */ (this.$.dialog).showModal();
},
/**
* @return {!Date} The date that is currently displayed on the dialog.
* @private
*/
getInputTime_() {
// Midnight of the current day in GMT.
const date = this.$.dateInput.valueAsDate;
// Add hours and minutes as set on the time input field.
date.setMilliseconds(this.$.timeInput.valueAsNumber);
// Add seconds from the system time, since the input fields only allow
// setting hour and minute.
date.setSeconds(date.getSeconds() + new Date().getSeconds());
// Add timezone offset to get real time.
date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
return date;
},
/**
* @return {!number} Seconds since epoch representing the date on the dialog
* inputs.
* @private
*/
getInputTimeSinceEpoch_() {
const now = this.getInputTime_();
if (this.isTimezoneVisible_) {
// Add timezone offset to get real time. This is only necessary when the
// timezone was updated, which is only possible when the dropdown is
// visible.
const timezoneDelta = getTimezoneDelta(
/** @type {string} */ (loadTimeData.getValue('currentTimezoneId')),
this.selectedTimezone_);
now.setMilliseconds(now.getMilliseconds() + timezoneDelta);
}
return Math.floor(now / 1000);
},
/**
* Sets the current timezone.
* @param {string} timezoneId The timezone ID to select.
* @private
*/
setTimezone_(timezoneId) {
if (this.isTimezoneVisible_) {
const timezoneSelect = this.$$('#timezoneSelect');
assert(timezoneSelect.childElementCount > 0);
timezoneSelect.value = timezoneId;
}
const now = this.getInputTime_();
const timezoneDelta = getTimezoneDelta(timezoneId, this.selectedTimezone_);
now.setMilliseconds(now.getMilliseconds() + timezoneDelta);
this.selectedTimezone_ = timezoneId;
this.updateTime_(now);
},
/**
* Updates the date/time controls time.
* Called initially, then called again once a minute.
* @param {!Date} newTime Time used to update the date/time controls.
* @private
*/
updateTime_(newTime) {
// Only update time controls if neither is focused.
if (document.activeElement.id != 'dateInput' &&
document.activeElement.id != 'timeInput') {
const htmlValues = dateToHtmlValues(newTime);
this.prevValues_.dateInput = this.$.dateInput.value = htmlValues.date;
this.prevValues_.timeInput = this.$.timeInput.value = htmlValues.time;
}
if (this.timeTimeoutId_) {
window.clearTimeout(this.timeTimeoutId_);
}
// Start timer to update these inputs every minute.
const secondsRemaining = 60 - newTime.getSeconds();
const nextTime =
new Date(newTime.setSeconds(newTime.getSeconds() + secondsRemaining));
this.timeTimeoutId_ = window.setTimeout(
this.updateTime_.bind(this, nextTime), secondsRemaining * 1000);
},
/**
* Sets the system time from the UI.
* @private
*/
applyTime_() {
this.browserProxy_.setTimeInSeconds(this.getInputTimeSinceEpoch_());
},
/**
* Called when focus is lost on date/time controls.
* @param {!Event} e The blur event.
* @private
*/
onTimeBlur_(e) {
if (e.target.validity.valid && e.target.value) {
// Make this the new fallback time in case of future invalid input.
this.prevValues_[e.target.id] = e.target.value;
} else {
// Restore previous value.
e.target.value = this.prevValues_[e.target.id];
}
// Schedule periodic updates with the new time.
this.updateTime_(this.getInputTime_());
},
/**
* @param {!Event} e The change event.
* @private
*/
onTimezoneChange_(e) {
this.setTimezone_(e.currentTarget.value);
},
/**
* Called when the done button is clicked. Child accounts need parental
* approval to change time, which requires an extra step after the button is
* clicked. This method notifies the dialog delegate to start the approval
* step, once the approval is granted the 'validation-complete' event is
* triggered invoking saveAndClose_. For regular accounts, this step is
* skipped and saveAndClose_ is called immediately after the button click.
* @private
*/
onDoneClick_() {
this.browserProxy_.doneClicked(this.getInputTimeSinceEpoch_());
},
/** @private */
saveAndClose_() {
this.applyTime_();
// Timezone change should only be applied when the UI displays timezone
// setting. Otherwise |selectedTimezone_| will be empty/invalid.
if (this.isTimezoneVisible_) {
this.browserProxy_.setTimezone(this.selectedTimezone_);
}
this.browserProxy_.dialogClose();
},
});