Bug 1512489 - Convert datetime-popup binding to a JSM r=Felipe

Differential Revision: https://phabricator.services.mozilla.com/D13944

--HG--
rename : toolkit/content/widgets/datetimepopup.xml => toolkit/modules/DateTimePickerPanel.jsm
extra : moz-landing-system : lando
This commit is contained in:
Timothy Guan-tin Chien 2018-12-21 21:51:57 +00:00
Родитель 12e4f91411
Коммит 9292b73037
7 изменённых файлов: 327 добавлений и 369 удалений

Просмотреть файл

@ -739,10 +739,6 @@ html|input.urlbar-input {
transition: none;
}
#DateTimePickerPanel[active="true"] {
-moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup");
}
#urlbar[pageproxystate=invalid] > #page-action-buttons > .urlbar-page-action,
#identity-box.chromeUI ~ #page-action-buttons > .urlbar-page-action:not(#star-button-box),
.urlbar-history-dropmarker[usertyping],

Просмотреть файл

@ -68,7 +68,6 @@ toolkit.jar:
content/global/bindings/checkbox.xml (widgets/checkbox.xml)
content/global/bindings/datekeeper.js (widgets/datekeeper.js)
content/global/bindings/datepicker.js (widgets/datepicker.js)
content/global/bindings/datetimepopup.xml (widgets/datetimepopup.xml)
content/global/bindings/datetimebox.xml (widgets/datetimebox.xml)
content/global/bindings/datetimebox.css (widgets/datetimebox.css)
* content/global/bindings/dialog.xml (widgets/dialog.xml)

Просмотреть файл

@ -142,14 +142,7 @@ class DateTimeTestHelper {
async openPicker(pageUrl) {
this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser);
// If dateTimePopupFrame doesn't exist yet, wait for the binding to be
// attached.
// FIXME: This has a race condition and we may miss the following events.
// (bug 1423498)
if (!this.panel.dateTimePopupFrame) {
await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady");
}
this.frame = this.panel.dateTimePopupFrame;
this.frame = this.panel.querySelector("#dateTimePopupFrame");
await this.waitForPickerReady();
}
@ -207,7 +200,6 @@ class DateTimeTestHelper {
this.panel.addEventListener("popuphidden", resolve, {once: true});
});
this.panel.hidePopup();
this.panel.closePicker();
await pickerClosePromise;
}
BrowserTestUtils.removeTab(this.tab);

Просмотреть файл

@ -1,337 +0,0 @@
<?xml version="1.0"?>
<!-- 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/. -->
<bindings id="dateTimePopupBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="datetime-popup"
extends="chrome://global/content/bindings/popup.xml#arrowpanel">
<implementation>
<property name="dateTimePopupFrame">
<getter>
let frame = this.querySelector("#dateTimePopupFrame");
if (!frame) {
frame = this.ownerDocument.createXULElement("iframe");
frame.id = "dateTimePopupFrame";
this.appendChild(frame);
}
return frame;
</getter>
</property>
<field name="TIME_PICKER_WIDTH" readonly="true">"12em"</field>
<field name="TIME_PICKER_HEIGHT" readonly="true">"21em"</field>
<field name="DATE_PICKER_WIDTH" readonly="true">"23.1em"</field>
<field name="DATE_PICKER_HEIGHT" readonly="true">"20.7em"</field>
<constructor><![CDATA[
this.mozIntl = Cc["@mozilla.org/mozintl;1"]
.getService(Ci.mozIMozIntl);
// Notify DateTimePickerParent.jsm that binding is ready.
this.dispatchEvent(new CustomEvent("DateTimePickerBindingReady"));
]]></constructor>
<method name="openPicker">
<parameter name="type"/>
<parameter name="anchor"/>
<parameter name="detail"/>
<body><![CDATA[
this.type = type;
this.pickerState = {};
// TODO: Resize picker according to content zoom level
this.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.hidden = false;
this.openPopup(anchor, "after_start", 0, 0);
]]></body>
</method>
<method name="closePicker">
<body><![CDATA[
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.hidden = true;
]]></body>
</method>
<method name="setPopupValue">
<parameter name="data"/>
<body><![CDATA[
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;
}
}
]]></body>
</method>
<method name="initPicker">
<parameter name="detail"/>
<body><![CDATA[
// TODO: When bug 1376616 lands, replace this.setGregorian with
// mozIntl.Locale for setting calendar to Gregorian
const locale = this.setGregorian(Services.locale.appLocaleAsBCP47);
const dir = this.mozIntl.getLocaleInfo(locale).direction;
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;
}
}
]]></body>
</method>
<method name="setInputBoxValue">
<parameter name="passAllValues"/>
<body><![CDATA[
/**
* @param {Boolean} passAllValues: Pass spinner values regardless if they've been set/changed or not
*/
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;
}
}
]]></body>
</method>
<method name="sendPickerValueChanged">
<parameter name="value"/>
<body><![CDATA[
switch (this.type) {
case "time": {
this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", {
detail: {
hour: value.hour,
minute: value.minute,
},
}));
break;
}
case "date": {
this.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;
}
}
]]></body>
</method>
<method name="getCalendarInfo">
<parameter name="locale"/>
<body><![CDATA[
const calendarInfo = this.mozIntl.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,
};
]]></body>
</method>
<method name="getDisplayNames">
<parameter name="locale"/>
<parameter name="keys"/>
<parameter name="style"/>
<body><![CDATA[
const displayNames = this.mozIntl.getDisplayNames(locale, {keys, style});
return keys.map(key => displayNames.values[key]);
]]></body>
</method>
<method name="setGregorian">
<parameter name="locale"/>
<body><![CDATA[
if (locale.match(/u-ca-/)) {
return locale.replace(/u-ca-[^-]+/, "u-ca-gregory");
}
return locale + "-u-ca-gregory";
]]></body>
</method>
<method name="handleEvent">
<parameter name="aEvent"/>
<body><![CDATA[
switch (aEvent.type) {
case "load": {
this.initPicker(this.detail);
this.dateTimePopupFrame.contentWindow.addEventListener("message", this);
break;
}
case "message": {
this.handleMessage(aEvent);
break;
}
}
]]></body>
</method>
<method name="handleMessage">
<parameter name="aEvent"/>
<body><![CDATA[
if (!this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
return;
}
switch (aEvent.data.name) {
case "PickerPopupChanged": {
this.pickerState = aEvent.data.detail;
this.setInputBoxValue();
break;
}
case "ClosePopup": {
this.hidePopup();
this.closePicker();
break;
}
}
]]></body>
</method>
<method name="postMessageToPicker">
<parameter name="data"/>
<body><![CDATA[
if (this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
}
]]></body>
</method>
</implementation>
</binding>
</bindings>

Просмотреть файл

@ -0,0 +1,317 @@
/* 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",
];
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, anchor, 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.hidden = false;
this.element.openPopup(anchor, "after_start", 0, 0);
}
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.hidden = true;
}
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) {
// TODO: When bug 1376616 lands, replace this.setGregorian with
// mozIntl.Locale for setting calendar to Gregorian
const locale = this.setGregorian(Services.locale.appLocaleAsBCP47);
const dir = Services.intl.getLocaleInfo(locale).direction;
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]);
}
setGregorian(locale) {
if (locale.match(/u-ca-/)) {
return locale.replace(/u-ca-[^-]+/, "u-ca-gregory");
}
return locale + "-u-ca-gregory";
}
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.element.hidePopup();
this.closePicker();
break;
}
}
}
postMessageToPicker(data) {
if (this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
}
}
};

Просмотреть файл

@ -16,6 +16,7 @@ var EXPORTED_SYMBOLS = [
];
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "DateTimePickerPanel", "resource://gre/modules/DateTimePickerPanel.jsm");
/*
* DateTimePickerParent receives message from content side (input box) and
@ -105,7 +106,7 @@ var DateTimePickerParent = {
},
// Get picker from browser and show it anchored to the input box.
async showPicker(aBrowser, aData) {
showPicker(aBrowser, aData) {
let rect = aData.rect;
let type = aData.type;
let detail = aData.detail;
@ -129,22 +130,11 @@ var DateTimePickerParent = {
}
this.weakBrowser = Cu.getWeakReference(aBrowser);
this.picker = aBrowser.dateTimePicker;
if (!this.picker) {
if (!aBrowser.dateTimePicker) {
debug("aBrowser.dateTimePicker not found, exiting now.");
return;
}
// The datetimepopup binding is only attached when it is needed.
// Check if openPicker method is present to determine if binding has
// been attached. If not, attach the binding first before calling it.
if (!this.picker.openPicker) {
let bindingPromise = new Promise(resolve => {
this.picker.addEventListener("DateTimePickerBindingReady",
resolve, {once: true});
});
this.picker.setAttribute("active", true);
await bindingPromise;
}
this.picker = new DateTimePickerPanel(aBrowser.dateTimePicker);
// The arrow panel needs an anchor to work. The popupAnchor (this._anchor)
// is a transparent div that the arrow can point to.
this.picker.openPicker(type, this._anchor, detail);
@ -165,8 +155,8 @@ var DateTimePickerParent = {
if (!this.picker) {
return;
}
this.picker.addEventListener("popuphidden", this);
this.picker.addEventListener("DateTimePickerValueChanged", this);
this.picker.element.addEventListener("popuphidden", this);
this.picker.element.addEventListener("DateTimePickerValueChanged", this);
},
// Stop listening to picker's event.
@ -174,7 +164,7 @@ var DateTimePickerParent = {
if (!this.picker) {
return;
}
this.picker.removeEventListener("popuphidden", this);
this.picker.removeEventListener("DateTimePickerValueChanged", this);
this.picker.element.removeEventListener("popuphidden", this);
this.picker.element.removeEventListener("DateTimePickerValueChanged", this);
},
};

Просмотреть файл

@ -194,6 +194,7 @@ EXTRA_JS_MODULES += [
'Console.jsm',
'CreditCard.jsm',
'css-selector.js',
'DateTimePickerPanel.jsm',
'DateTimePickerParent.jsm',
'DeferredTask.jsm',
'Deprecated.jsm',