fune/toolkit/modules/DateTimePickerPanel.jsm
Emilio Cobos Álvarez fe58c377b3 Bug 1694413 - Use hidePopup rather than the hidden attribute to hide datetime picker. r=Gijs
On macos re-showing it without using `hidden` can cause, apparently,
extra popuphidden events. Let's do the right thing and close the popup
properly using hidePopup().

This code wasn't being hit before bug 1339380 because we relied on the
parent side to hide it, which did correctly fire the events.

Differential Revision: https://phabricator.services.mozilla.com/D106431
2021-02-25 14:13:14 +00:00

338 lines
9.3 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["DateTimePickerPanel"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var DateTimePickerPanel = class {
constructor(element) {
this.element = element;
this.TIME_PICKER_WIDTH = "12em";
this.TIME_PICKER_HEIGHT = "21em";
this.DATE_PICKER_WIDTH = "23.1em";
this.DATE_PICKER_HEIGHT = "20.7em";
}
get dateTimePopupFrame() {
let frame = this.element.querySelector("#dateTimePopupFrame");
if (!frame) {
frame = this.element.ownerDocument.createXULElement("iframe");
frame.id = "dateTimePopupFrame";
this.element.appendChild(frame);
}
return frame;
}
openPicker(type, rect, detail) {
this.type = type;
this.pickerState = {};
// TODO: Resize picker according to content zoom level
this.element.style.fontSize = "10px";
switch (type) {
case "time": {
this.detail = detail;
this.dateTimePopupFrame.addEventListener("load", this, true);
this.dateTimePopupFrame.setAttribute(
"src",
"chrome://global/content/timepicker.xhtml"
);
this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH;
this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT;
break;
}
case "date": {
this.detail = detail;
this.dateTimePopupFrame.addEventListener("load", this, true);
this.dateTimePopupFrame.setAttribute(
"src",
"chrome://global/content/datepicker.xhtml"
);
this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH;
this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT;
break;
}
}
this.element.openPopupAtScreenRect(
"after_start",
rect.left,
rect.top,
rect.width,
rect.height,
false,
false
);
}
closePicker() {
this.setInputBoxValue(true);
this.pickerState = {};
this.type = undefined;
this.dateTimePopupFrame.removeEventListener("load", this, true);
this.dateTimePopupFrame.contentDocument.removeEventListener(
"message",
this
);
this.dateTimePopupFrame.setAttribute("src", "");
this.element.hidePopup();
}
setPopupValue(data) {
switch (this.type) {
case "time": {
this.postMessageToPicker({
name: "PickerSetValue",
detail: data.value,
});
break;
}
case "date": {
const { year, month, day } = data.value;
this.postMessageToPicker({
name: "PickerSetValue",
detail: {
year,
// Month value from input box starts from 1 instead of 0
month: month == undefined ? undefined : month - 1,
day,
},
});
break;
}
}
}
initPicker(detail) {
let locale = new Services.intl.Locale(
Services.locale.webExposedLocales[0],
{
calendar: "gregory",
}
).toString();
// Workaround for bug 1418061, while we wait for resolution of
// http://bugs.icu-project.org/trac/ticket/13592: drop the PT region code,
// because it results in "abbreviated" day names that are too long;
// the region-less "pt" locale has shorter forms that are better here.
locale = locale.replace(/^pt-PT/i, "pt");
const dir = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
switch (this.type) {
case "time": {
const { hour, minute } = detail.value;
const format = detail.format || "12";
this.postMessageToPicker({
name: "PickerInit",
detail: {
hour,
minute,
format,
locale,
min: detail.min,
max: detail.max,
step: detail.step,
},
});
break;
}
case "date": {
const { year, month, day } = detail.value;
const { firstDayOfWeek, weekends } = this.getCalendarInfo(locale);
const monthStrings = this.getDisplayNames(
locale,
[
"dates/gregorian/months/january",
"dates/gregorian/months/february",
"dates/gregorian/months/march",
"dates/gregorian/months/april",
"dates/gregorian/months/may",
"dates/gregorian/months/june",
"dates/gregorian/months/july",
"dates/gregorian/months/august",
"dates/gregorian/months/september",
"dates/gregorian/months/october",
"dates/gregorian/months/november",
"dates/gregorian/months/december",
],
"short"
);
const weekdayStrings = this.getDisplayNames(
locale,
[
"dates/gregorian/weekdays/sunday",
"dates/gregorian/weekdays/monday",
"dates/gregorian/weekdays/tuesday",
"dates/gregorian/weekdays/wednesday",
"dates/gregorian/weekdays/thursday",
"dates/gregorian/weekdays/friday",
"dates/gregorian/weekdays/saturday",
],
"short"
);
this.postMessageToPicker({
name: "PickerInit",
detail: {
year,
// Month value from input box starts from 1 instead of 0
month: month == undefined ? undefined : month - 1,
day,
firstDayOfWeek,
weekends,
monthStrings,
weekdayStrings,
locale,
dir,
min: detail.min,
max: detail.max,
step: detail.step,
stepBase: detail.stepBase,
},
});
break;
}
}
}
/**
* @param {Boolean} passAllValues: Pass spinner values regardless if they've been set/changed or not
*/
setInputBoxValue(passAllValues) {
switch (this.type) {
case "time": {
const {
hour,
minute,
isHourSet,
isMinuteSet,
isDayPeriodSet,
} = this.pickerState;
const isAnyValueSet = isHourSet || isMinuteSet || isDayPeriodSet;
if (passAllValues && isAnyValueSet) {
this.sendPickerValueChanged({ hour, minute });
} else {
this.sendPickerValueChanged({
hour: isHourSet || isDayPeriodSet ? hour : undefined,
minute: isMinuteSet ? minute : undefined,
});
}
break;
}
case "date": {
this.sendPickerValueChanged(this.pickerState);
break;
}
}
}
sendPickerValueChanged(value) {
switch (this.type) {
case "time": {
this.element.dispatchEvent(
new CustomEvent("DateTimePickerValueChanged", {
detail: {
hour: value.hour,
minute: value.minute,
},
})
);
break;
}
case "date": {
this.element.dispatchEvent(
new CustomEvent("DateTimePickerValueChanged", {
detail: {
year: value.year,
// Month value from input box starts from 1 instead of 0
month: value.month == undefined ? undefined : value.month + 1,
day: value.day,
},
})
);
break;
}
}
}
getCalendarInfo(locale) {
const calendarInfo = Services.intl.getCalendarInfo(locale);
// Day of week from calendarInfo starts from 1 as Sunday to 7 as Saturday,
// so they need to be mapped to JavaScript convention with 0 as Sunday
// and 6 as Saturday
let firstDayOfWeek = calendarInfo.firstDayOfWeek - 1,
weekendStart = calendarInfo.weekendStart - 1,
weekendEnd = calendarInfo.weekendEnd - 1;
let weekends = [];
// Make sure weekendEnd is greater than weekendStart
if (weekendEnd < weekendStart) {
weekendEnd += 7;
}
// We get the weekends by incrementing weekendStart up to weekendEnd.
// If the start and end is the same day, then weekends only has one day.
for (let day = weekendStart; day <= weekendEnd; day++) {
weekends.push(day % 7);
}
return {
firstDayOfWeek,
weekends,
};
}
getDisplayNames(locale, keys, style) {
const displayNames = Services.intl.getDisplayNames(locale, { keys, style });
return keys.map(key => displayNames.values[key]);
}
handleEvent(aEvent) {
switch (aEvent.type) {
case "load": {
this.initPicker(this.detail);
this.dateTimePopupFrame.contentWindow.addEventListener("message", this);
break;
}
case "message": {
this.handleMessage(aEvent);
break;
}
}
}
handleMessage(aEvent) {
if (
!this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
) {
return;
}
switch (aEvent.data.name) {
case "PickerPopupChanged": {
this.pickerState = aEvent.data.detail;
this.setInputBoxValue();
break;
}
case "ClosePopup": {
this.closePicker();
break;
}
}
}
postMessageToPicker(data) {
if (
this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
) {
this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
}
}
};