commit 16894a9e464cc5492d773f885716a6e8a9f59135
parent 679bcbcb587b3a7692b3f0744ba9b3207a5ad2da
Author: Kagami Sascha Rosylight <krosylight@proton.me>
Date: Wed, 17 Dec 2025 15:53:56 +0000
Bug 2004409 - Split window opener logic from DateTimePicker classes r=emilio
Differential Revision: https://phabricator.services.mozilla.com/D275250
Diffstat:
9 files changed, 669 insertions(+), 441 deletions(-)
diff --git a/toolkit/actors/DateTimePickerChild.sys.mjs b/toolkit/actors/DateTimePickerChild.sys.mjs
@@ -2,77 +2,30 @@
* 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/. */
-const lazy = {};
-ChromeUtils.defineESModuleGetters(lazy, {
- LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
-});
+import { InputPickerChildCommon } from "./InputPickerChildCommon.sys.mjs";
-/**
- * DateTimePickerChild is the communication channel between the input box
- * (content) for date/time input types and its picker (chrome).
- */
-export class DateTimePickerChild extends JSWindowActorChild {
- /**
- * On init, just listen for the event to open the picker, once the picker is
- * opened, we'll listen for update and close events.
- */
+export class DateTimePickerChild extends InputPickerChildCommon {
constructor() {
- super();
-
- this._inputElement = null;
+ super("DateTimePicker");
}
/**
* Cleanup function called when picker is closed.
+ *
+ * @param {HTMLInputElement} inputElement
*/
- close() {
- this.removeListeners(this._inputElement);
- let dateTimeBoxElement = this._inputElement.dateTimeBoxElement;
+ closeImpl(inputElement) {
+ let dateTimeBoxElement = inputElement.dateTimeBoxElement;
if (!dateTimeBoxElement) {
- this._inputElement = null;
return;
}
// dateTimeBoxElement is within UA Widget Shadow DOM.
// An event dispatch to it can't be accessed by document.
- let win = this._inputElement.ownerGlobal;
+ let win = inputElement.ownerGlobal;
dateTimeBoxElement.dispatchEvent(
new win.CustomEvent("MozSetDateTimePickerState", { detail: false })
);
-
- this._inputElement = null;
- }
-
- /**
- * Called after picker is opened to start listening for input box update
- * events.
- */
- addListeners(aElement) {
- aElement.ownerGlobal.addEventListener("pagehide", this);
- }
-
- /**
- * Stop listeneing for events when picker is closed.
- */
- removeListeners(aElement) {
- aElement.ownerGlobal.removeEventListener("pagehide", this);
- }
-
- /**
- * Helper function that returns the CSS direction property of the element.
- */
- getComputedDirection(aElement) {
- return aElement.ownerGlobal
- .getComputedStyle(aElement)
- .getPropertyValue("direction");
- }
-
- /**
- * Helper function that returns the rect of the element, which is the position
- * relative to the left/top of the content area.
- */
- getBoundingContentRect(aElement) {
- return lazy.LayoutUtils.getElementBoundingScreenRect(aElement);
}
getTimePickerPref() {
@@ -80,119 +33,62 @@ export class DateTimePickerChild extends JSWindowActorChild {
}
/**
- * nsIMessageListener.
+ * Element updater function called when the picker value is changed.
+ *
+ * @param {ReceiveMessageArgument} aMessage
+ * @param {HTMLInputElement} inputElement
*/
- receiveMessage(aMessage) {
- switch (aMessage.name) {
- case "FormDateTime:PickerClosed": {
- if (!this._inputElement) {
- return;
- }
-
- this.close();
- break;
- }
- case "FormDateTime:PickerValueChanged": {
- if (!this._inputElement) {
- return;
- }
-
- let dateTimeBoxElement = this._inputElement.dateTimeBoxElement;
- if (!dateTimeBoxElement) {
- return;
- }
+ pickerValueChangedImpl(aMessage, inputElement) {
+ let dateTimeBoxElement = inputElement.dateTimeBoxElement;
+ if (!dateTimeBoxElement) {
+ return;
+ }
- let win = this._inputElement.ownerGlobal;
+ let win = inputElement.ownerGlobal;
- // dateTimeBoxElement is within UA Widget Shadow DOM.
- // An event dispatch to it can't be accessed by document.
- dateTimeBoxElement.dispatchEvent(
- new win.CustomEvent("MozPickerValueChanged", {
- detail: Cu.cloneInto(aMessage.data, win),
- })
- );
- break;
- }
- default:
- break;
- }
+ // dateTimeBoxElement is within UA Widget Shadow DOM.
+ // An event dispatch to it can't be accessed by document.
+ dateTimeBoxElement.dispatchEvent(
+ new win.CustomEvent("MozPickerValueChanged", {
+ detail: Cu.cloneInto(aMessage.data, win),
+ })
+ );
}
/**
- * nsIDOMEventListener, for chrome events sent by the input element and other
- * DOM events.
+ * Picker initialization function called when opening the picker
+ *
+ * @param {HTMLInputElement} inputElement
+ * @returns An argument object to pass to the picker panel, or undefined to stop.
*/
- handleEvent(aEvent) {
- switch (aEvent.type) {
- case "MozOpenDateTimePicker": {
- // Time picker is disabled when preffed off
- if (
- !aEvent.originalTarget.ownerGlobal.HTMLInputElement.isInstance(
- aEvent.originalTarget
- ) ||
- (aEvent.originalTarget.type == "time" && !this.getTimePickerPref())
- ) {
- return;
- }
-
- if (this._inputElement) {
- // This happens when we're trying to open a picker when another picker
- // is still open. We ignore this request to let the first picker
- // close gracefully.
- return;
- }
-
- this._inputElement = aEvent.originalTarget;
-
- let dateTimeBoxElement = this._inputElement.dateTimeBoxElement;
- if (!dateTimeBoxElement) {
- throw new Error("How do we get this event without a UA Widget?");
- }
+ openPickerImpl(inputElement) {
+ // Time picker is disabled when preffed off
+ if (inputElement.type == "time" && !this.getTimePickerPref()) {
+ return undefined;
+ }
- // dateTimeBoxElement is within UA Widget Shadow DOM.
- // An event dispatch to it can't be accessed by document, because
- // the event is not composed.
- let win = this._inputElement.ownerGlobal;
- dateTimeBoxElement.dispatchEvent(
- new win.CustomEvent("MozSetDateTimePickerState", { detail: true })
- );
+ let dateTimeBoxElement = inputElement.dateTimeBoxElement;
+ if (!dateTimeBoxElement) {
+ throw new Error("How do we get this event without a UA Widget?");
+ }
- this.addListeners(this._inputElement);
+ // dateTimeBoxElement is within UA Widget Shadow DOM.
+ // An event dispatch to it can't be accessed by document, because
+ // the event is not composed.
+ let win = inputElement.ownerGlobal;
+ dateTimeBoxElement.dispatchEvent(
+ new win.CustomEvent("MozSetDateTimePickerState", { detail: true })
+ );
- let value = this._inputElement.getDateTimeInputBoxValue();
- this.sendAsyncMessage("FormDateTime:OpenPicker", {
- rect: this.getBoundingContentRect(this._inputElement),
- dir: this.getComputedDirection(this._inputElement),
- type: this._inputElement.type,
- detail: {
- // Pass partial value if it's available, otherwise pass input
- // element's value.
- value: Object.keys(value).length ? value : this._inputElement.value,
- min: this._inputElement.getMinimum(),
- max: this._inputElement.getMaximum(),
- step: this._inputElement.getStep(),
- stepBase: this._inputElement.getStepBase(),
- },
- });
- break;
- }
- case "MozCloseDateTimePicker": {
- this.sendAsyncMessage("FormDateTime:ClosePicker", {});
- this.close();
- break;
- }
- case "pagehide": {
- if (
- this._inputElement &&
- this._inputElement.ownerDocument == aEvent.target
- ) {
- this.sendAsyncMessage("FormDateTime:ClosePicker", {});
- this.close();
- }
- break;
- }
- default:
- break;
- }
+ let value = inputElement.getDateTimeInputBoxValue();
+ return {
+ // Pass partial value if it's available, otherwise pass input
+ // element's value.
+ value: Object.keys(value).length ? value : inputElement.value,
+ min: inputElement.getMinimum(),
+ max: inputElement.getMaximum(),
+ step: inputElement.getStep(),
+ stepBase: inputElement.getStepBase(),
+ };
}
}
diff --git a/toolkit/actors/DateTimePickerParent.sys.mjs b/toolkit/actors/DateTimePickerParent.sys.mjs
@@ -2,141 +2,25 @@
* 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/. */
-const DEBUG = false;
-function debug(aStr) {
- if (DEBUG) {
- dump("-*- DateTimePickerParent: " + aStr + "\n");
- }
-}
+import { InputPickerParentCommon } from "./InputPickerParentCommon.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- DateTimePickerPanel: "resource://gre/modules/DateTimePickerPanel.sys.mjs",
+ DateTimePickerPanel: "moz-src:///toolkit/modules/DateTimePickerPanel.sys.mjs",
});
-/*
- * DateTimePickerParent receives message from content side (input box) and
- * is reposible for opening, closing and updating the picker. Similarly,
- * DateTimePickerParent listens for picker's events and notifies the content
- * side (input box) about them.
- */
-export class DateTimePickerParent extends JSWindowActorParent {
- receiveMessage(aMessage) {
- debug("receiveMessage: " + aMessage.name);
- switch (aMessage.name) {
- case "FormDateTime:OpenPicker": {
- this.showPicker(aMessage.data);
- break;
- }
- case "FormDateTime:ClosePicker": {
- if (!this._picker) {
- return;
- }
- this.close();
- break;
- }
- default:
- break;
- }
- }
-
- handleEvent(aEvent) {
- debug("handleEvent: " + aEvent.type);
- switch (aEvent.type) {
- case "DateTimePickerValueCleared": {
- this.sendAsyncMessage("FormDateTime:PickerValueChanged", null);
- break;
- }
- case "DateTimePickerValueChanged": {
- this.sendAsyncMessage("FormDateTime:PickerValueChanged", aEvent.detail);
- break;
- }
- case "popuphidden": {
- this.sendAsyncMessage("FormDateTime:PickerClosed", {});
- this.close();
- break;
- }
- default:
- break;
- }
- }
-
- // Get picker from browser and show it anchored to the input box.
- showPicker(aData) {
- let rect = aData.rect;
- let type = aData.type;
- let detail = aData.detail;
-
- debug("Opening picker with details: " + JSON.stringify(detail));
- let topBC = this.browsingContext.top;
- let window = topBC.topChromeWindow;
- if (Services.focus.activeWindow != window) {
- debug("Not in the active window");
- return;
- }
-
- {
- let browser = topBC.embedderElement;
- if (
- browser &&
- browser.ownerGlobal.gBrowser &&
- browser.ownerGlobal.gBrowser.selectedBrowser != browser
- ) {
- debug("In background tab");
- return;
- }
- }
-
- this._cleanupPicker();
- let doc = window.document;
- let panel = doc.getElementById("DateTimePickerPanel");
- if (!panel) {
- panel = doc.createXULElement("panel");
- panel.id = "DateTimePickerPanel";
- panel.setAttribute("type", "arrow");
- panel.setAttribute("orient", "vertical");
- panel.setAttribute("ignorekeys", "true");
- panel.setAttribute("noautofocus", "true");
- // This ensures that clicks on the anchored input box are never consumed.
- panel.setAttribute("consumeoutsideclicks", "never");
- panel.setAttribute("level", "parent");
- panel.setAttribute("tabspecific", "true");
- let container =
- doc.getElementById("mainPopupSet") ||
- doc.querySelector("popupset") ||
- doc.documentElement.appendChild(doc.createXULElement("popupset"));
- container.appendChild(panel);
- }
- this._oldFocus = doc.activeElement;
- this._picker = new lazy.DateTimePickerPanel(panel);
- this._picker.openPicker(type, rect, detail);
- this._picker.element.addEventListener("popuphidden", this);
- this._picker.element.addEventListener("DateTimePickerValueChanged", this);
- this._picker.element.addEventListener("DateTimePickerValueCleared", this);
- }
-
- _cleanupPicker() {
- if (!this._picker) {
- return;
- }
- this._picker.closePicker();
- this._picker.element.removeEventListener("popuphidden", this);
- this._picker.element.removeEventListener(
- "DateTimePickerValueChanged",
- this
- );
- this._picker.element.removeEventListener(
- "DateTimePickerValueCleared",
- this
- );
- this._picker = null;
+export class DateTimePickerParent extends InputPickerParentCommon {
+ constructor() {
+ super("DateTimePicker");
}
- // Close the picker and do some cleanup.
- close() {
- this._cleanupPicker();
- // Restore focus to where it was before the picker opened.
- this._oldFocus?.focus();
- this._oldFocus = null;
+ /**
+ * A picker creator function called when showing a picker
+ *
+ * @param {XULElement} panel A panel element
+ * @returns A panel object that manages the element
+ */
+ createPickerImpl(panel) {
+ return new lazy.DateTimePickerPanel(panel);
}
}
diff --git a/toolkit/actors/InputPickerChildCommon.sys.mjs b/toolkit/actors/InputPickerChildCommon.sys.mjs
@@ -0,0 +1,188 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
+});
+
+/**
+ * InputPickerChildCommon is the communication channel between the input box
+ * (content) for each input types and its picker (chrome).
+ */
+export class InputPickerChildCommon extends JSWindowActorChild {
+ /** @type {HTMLInputElement} */
+ #inputElement = null;
+ #inputType = "";
+ #namespace;
+ /** @type {AbortController} */
+ #abortController;
+
+ /**
+ * On init, just listen for the event to open the picker, once the picker is
+ * opened, we'll listen for update and close events.
+ *
+ * @param {string} namespace Affects the event names, e.g. Foo makes it
+ * accept FooValueChanged event.
+ * Align it with ActorManagerParent declaration.
+ */
+ constructor(namespace) {
+ super();
+ this.#namespace = namespace;
+ }
+
+ /**
+ * Cleanup function called when picker is closed.
+ */
+ close() {
+ this.#abortController.abort();
+ this.closeImpl(this.#inputElement);
+ this.#inputElement = null;
+ this.#inputType = "";
+ }
+
+ /**
+ * @param {HTMLInputElement} _inputElement
+ */
+ closeImpl(_inputElement) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Called after picker is opened to start listening for input box update
+ * events.
+ */
+ addListeners(aElement) {
+ this.#abortController = new AbortController();
+ aElement.ownerGlobal.addEventListener("pagehide", this, {
+ signal: this.#abortController.signal,
+ });
+ }
+
+ /**
+ * Helper function that returns the CSS direction property of the element.
+ */
+ getComputedDirection(aElement) {
+ return aElement.ownerGlobal
+ .getComputedStyle(aElement)
+ .getPropertyValue("direction");
+ }
+
+ /**
+ * Helper function that returns the rect of the element, which is the position
+ * relative to the left/top of the content area.
+ */
+ getBoundingContentRect(aElement) {
+ return lazy.LayoutUtils.getElementBoundingScreenRect(aElement);
+ }
+
+ /**
+ * MessageListener
+ */
+ receiveMessage(aMessage) {
+ if (!this.#inputElement || this.#inputElement.type !== this.#inputType) {
+ // Either we are already closed by content or the input type is changed
+ return;
+ }
+ switch (aMessage.name) {
+ case "InputPicker:Closed": {
+ this.close();
+ break;
+ }
+ case "InputPicker:ValueChanged": {
+ this.pickerValueChangedImpl(aMessage, this.#inputElement);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Element updater function called when the picker value is changed.
+ *
+ * @param {ReceiveMessageArgument} _aMessage
+ * @param {HTMLInputElement} _inputElement
+ */
+ pickerValueChangedImpl(_aMessage, _inputElement) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * nsIDOMEventListener, for chrome events sent by the input element and other
+ * DOM events.
+ */
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case `MozOpen${this.#namespace}`: {
+ if (
+ !aEvent.originalTarget.ownerGlobal.HTMLInputElement.isInstance(
+ aEvent.originalTarget
+ )
+ ) {
+ return;
+ }
+
+ if (this.#inputElement) {
+ // This happens when we're trying to open a picker when another picker
+ // is still open. We ignore this request to let the first picker
+ // close gracefully.
+ return;
+ }
+
+ /** @type {HTMLInputElement} */
+ const inputElement = aEvent.originalTarget;
+ const openPickerDetail = this.openPickerImpl(inputElement);
+ if (!openPickerDetail) {
+ // The impl doesn't want to proceed in this case
+ return;
+ }
+
+ this.#inputElement = inputElement;
+ this.#inputType = inputElement.type;
+ this.addListeners(inputElement);
+
+ this.sendAsyncMessage(`InputPicker:Open`, {
+ rect: this.getBoundingContentRect(inputElement),
+ dir: this.getComputedDirection(inputElement),
+ type: inputElement.type,
+ detail: openPickerDetail,
+ });
+ break;
+ }
+ case `MozClose${this.#namespace}`: {
+ this.sendAsyncMessage(`InputPicker:Close`, {});
+ this.close();
+ break;
+ }
+ case "pagehide": {
+ if (this.#inputElement?.ownerDocument == aEvent.target) {
+ this.sendAsyncMessage(`InputPicker:Close`, {});
+ this.close();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Picker initialization function called when opening the picker
+ *
+ * @param {HTMLInputElement} _inputElement
+ * @returns An argument object to pass to the picker, or undefined to stop opening one.
+ */
+ openPickerImpl(_inputElement) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Picker updater function when the input value is updated
+ *
+ * @param {HTMLInputElement} _inputElement
+ * @returns An argument object to pass to the picker
+ */
+ updatePickerImpl(_inputElement) {
+ throw new Error("Not implemented");
+ }
+}
diff --git a/toolkit/actors/InputPickerParentCommon.sys.mjs b/toolkit/actors/InputPickerParentCommon.sys.mjs
@@ -0,0 +1,170 @@
+/* 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/. */
+
+const DEBUG = false;
+function debug(aStr) {
+ if (DEBUG) {
+ dump("-*- InputPickerParent: " + aStr + "\n");
+ }
+}
+
+/*
+ * InputPickerParentCommon receives message from content side (input box) and
+ * is reposible for opening, closing and updating the picker. Similarly,
+ * InputPickerParentCommon listens for picker's events and notifies the content
+ * side (input box) about them.
+ */
+export class InputPickerParentCommon extends JSWindowActorParent {
+ #namespace;
+ #picker;
+ /** @type {Element | undefined} */
+ #oldFocus;
+ /** @type {AbortController} */
+ #abortController;
+
+ /**
+ * @param {string} namespace Affects the input panel id, mostly to prevent
+ * accidental mis-pairing of wrong panel and actor.
+ */
+ constructor(namespace) {
+ super();
+ this.#namespace = namespace;
+ }
+
+ receiveMessage(aMessage) {
+ debug("receiveMessage: " + aMessage.name);
+ switch (aMessage.name) {
+ case `InputPicker:Open`: {
+ this.showPicker(aMessage.data);
+ break;
+ }
+ case `InputPicker:Close`: {
+ if (!this.#picker) {
+ return;
+ }
+ this.close();
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ handleEvent(aEvent) {
+ debug("handleEvent: " + aEvent.type);
+ switch (aEvent.type) {
+ case "InputPickerValueCleared": {
+ this.sendAsyncMessage("InputPicker:ValueChanged", null);
+ break;
+ }
+ case "InputPickerValueChanged": {
+ this.sendAsyncMessage("InputPicker:ValueChanged", aEvent.detail);
+ break;
+ }
+ case "popuphidden": {
+ this.sendAsyncMessage(`InputPicker:Closed`, {});
+ this.close();
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ /**
+ * A panel creator function called when showing a picker
+ *
+ * @param {XULElement} _panel A panel element
+ * @returns A panel object that manages the element
+ */
+ createPickerImpl(_panel) {
+ throw new Error("Not implemented");
+ }
+
+ // Get picker from browser and show it anchored to the input box.
+ showPicker(aData) {
+ let rect = aData.rect;
+ let type = aData.type;
+ let detail = aData.detail;
+
+ debug("Opening picker with details: " + JSON.stringify(detail));
+ let topBC = this.browsingContext.top;
+ let window = topBC.topChromeWindow;
+ if (Services.focus.activeWindow != window) {
+ debug("Not in the active window");
+ return;
+ }
+
+ {
+ let browser = topBC.embedderElement;
+ if (
+ browser &&
+ browser.ownerGlobal.gBrowser &&
+ browser.ownerGlobal.gBrowser.selectedBrowser != browser
+ ) {
+ debug("In background tab");
+ return;
+ }
+ }
+
+ this.#cleanupPicker();
+ let doc = window.document;
+ const id = `${this.#namespace}Panel`;
+ let panel = doc.getElementById(id);
+ if (!panel) {
+ panel = doc.createXULElement("panel");
+ panel.id = id;
+ panel.setAttribute("type", "arrow");
+ panel.setAttribute("orient", "vertical");
+ panel.setAttribute("ignorekeys", "true");
+ panel.setAttribute("noautofocus", "true");
+ // This ensures that clicks on the anchored input box are never consumed.
+ panel.setAttribute("consumeoutsideclicks", "never");
+ panel.setAttribute("level", "parent");
+ panel.setAttribute("tabspecific", "true");
+ let container =
+ doc.getElementById("mainPopupSet") ||
+ doc.querySelector("popupset") ||
+ doc.documentElement.appendChild(doc.createXULElement("popupset"));
+ container.appendChild(panel);
+ }
+ this.#oldFocus = doc.activeElement;
+ this.#picker = this.createPickerImpl(panel);
+ this.#picker.openPicker(type, rect, detail);
+ this.addPickerListeners(panel);
+ }
+
+ #cleanupPicker() {
+ if (!this.#picker) {
+ return;
+ }
+ this.#picker.closePicker();
+ this.#abortController.abort();
+ this.#picker = null;
+ }
+
+ // Close the picker and do some cleanup.
+ close() {
+ this.#cleanupPicker();
+ // Restore focus to where it was before the picker opened.
+ this.#oldFocus?.focus();
+ this.#oldFocus = null;
+ }
+
+ // Listen to picker's event.
+ addPickerListeners(panel) {
+ if (!this.#picker) {
+ return;
+ }
+ this.#abortController = new AbortController();
+ const { signal } = this.#abortController;
+ panel.addEventListener("popuphidden", this, { signal });
+ panel.addEventListener("InputPickerValueChanged", this, {
+ signal,
+ });
+ panel.addEventListener("InputPickerValueCleared", this, {
+ signal,
+ });
+ }
+}
diff --git a/toolkit/actors/moz.build b/toolkit/actors/moz.build
@@ -48,8 +48,6 @@ FINAL_TARGET_FILES.actors += [
"ContentMetaParent.sys.mjs",
"ControllersChild.sys.mjs",
"ControllersParent.sys.mjs",
- "DateTimePickerChild.sys.mjs",
- "DateTimePickerParent.sys.mjs",
"ExtFindChild.sys.mjs",
"FindBarChild.sys.mjs",
"FindBarParent.sys.mjs",
@@ -83,3 +81,10 @@ FINAL_TARGET_FILES.actors += [
"WebChannelChild.sys.mjs",
"WebChannelParent.sys.mjs",
]
+
+MOZ_SRC_FILES += [
+ "DateTimePickerChild.sys.mjs",
+ "DateTimePickerParent.sys.mjs",
+ "InputPickerChildCommon.sys.mjs",
+ "InputPickerParentCommon.sys.mjs",
+]
diff --git a/toolkit/modules/ActorManagerParent.sys.mjs b/toolkit/modules/ActorManagerParent.sys.mjs
@@ -694,11 +694,11 @@ if (AppConstants.platform != "android") {
// Note that GeckoView handles MozOpenDateTimePicker in GeckoViewPrompt.
JSWINDOWACTORS.DateTimePicker = {
parent: {
- esModuleURI: "resource://gre/actors/DateTimePickerParent.sys.mjs",
+ esModuleURI: "moz-src:///toolkit/actors/DateTimePickerParent.sys.mjs",
},
child: {
- esModuleURI: "resource://gre/actors/DateTimePickerChild.sys.mjs",
+ esModuleURI: "moz-src:///toolkit/actors/DateTimePickerChild.sys.mjs",
events: {
MozOpenDateTimePicker: {},
MozCloseDateTimePicker: {},
diff --git a/toolkit/modules/DateTimePickerPanel.sys.mjs b/toolkit/modules/DateTimePickerPanel.sys.mjs
@@ -2,86 +2,69 @@
* 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/. */
-export class DateTimePickerPanel {
- constructor(element) {
- this.element = element;
+import { InputPickerPanelCommon } from "./InputPickerPanelCommon.sys.mjs";
- this.TIME_PICKER_WIDTH = "13em";
- this.TIME_PICKER_HEIGHT = "22em";
- this.DATE_PICKER_WIDTH = "24em";
- this.DATE_PICKER_HEIGHT = "27em";
- this.DATETIME_PICKER_WIDTH = "40em";
- this.DATETIME_PICKER_HEIGHT = "27em";
- }
+/** @import {OpenPickerInfo} from "./InputPickerPanelCommon.sys.mjs" */
- 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;
+const TIME_PICKER_WIDTH = "13em";
+const TIME_PICKER_HEIGHT = "22em";
+const DATE_PICKER_WIDTH = "24em";
+const DATE_PICKER_HEIGHT = "27em";
+const DATETIME_PICKER_WIDTH = "40em";
+const DATETIME_PICKER_HEIGHT = "27em";
+
+export class DateTimePickerPanel extends InputPickerPanelCommon {
+ constructor(element) {
+ super(element, "datetimepicker.xhtml");
}
- openPicker(type, rect, detail) {
+ /**
+ * Picker window initialization function called when opening the picker
+ *
+ * @param {string} type The input element type
+ * @returns {OpenPickerInfo}
+ */
+ openPickerImpl(type) {
if (
type == "datetime-local" &&
!Services.prefs.getBoolPref("dom.forms.datetime.timepicker")
) {
type = "date";
}
- this.pickerState = {};
- // TODO: Resize picker according to content zoom level
- this.element.style.fontSize = "10px";
- this.type = type;
- this.detail = detail;
- this.dateTimePopupFrame.addEventListener("load", this, true);
- this.dateTimePopupFrame.setAttribute(
- "src",
- "chrome://global/content/datetimepicker.xhtml"
- );
switch (type) {
case "time": {
- this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH;
- this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT;
- break;
+ return {
+ type,
+ width: TIME_PICKER_WIDTH,
+ height: TIME_PICKER_HEIGHT,
+ };
}
case "date": {
- this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH;
- this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT;
- break;
+ return {
+ type,
+ width: DATE_PICKER_WIDTH,
+ height: DATE_PICKER_HEIGHT,
+ };
}
case "datetime-local": {
- this.dateTimePopupFrame.style.width = this.DATETIME_PICKER_WIDTH;
- this.dateTimePopupFrame.style.height = this.DATETIME_PICKER_HEIGHT;
- break;
+ return {
+ type,
+ width: DATETIME_PICKER_WIDTH,
+ height: DATETIME_PICKER_HEIGHT,
+ };
}
}
- this.element.openPopupAtScreenRect(
- "after_start",
- rect.left,
- rect.top,
- rect.width,
- rect.height,
- false,
- false
- );
- }
-
- closePicker(clear) {
- if (clear) {
- this.element.dispatchEvent(new CustomEvent("DateTimePickerValueCleared"));
- }
- this.pickerState = {};
- this.type = undefined;
- this.dateTimePopupFrame.removeEventListener("load", this, true);
- this.dateTimePopupFrame.contentWindow.removeEventListener("message", this);
- this.dateTimePopupFrame.setAttribute("src", "");
- this.element.hidePopup();
+ throw new Error(`Unexpected type ${type}`);
}
- initPicker(detail) {
+ /**
+ * Popup frame initialization function called when the picker window is loaded
+ *
+ * @param {string} type The picker type
+ * @param {object} detail The argument from the child actor's openPickerImpl
+ * @returns An argument object to pass to the popup frame
+ */
+ initPickerImpl(type, detail) {
let locale = new Services.intl.Locale(
Services.locale.webExposedLocales[0],
{
@@ -99,7 +82,7 @@ export class DateTimePickerPanel {
const { year, month, day, hour, minute } = detail.value;
const flattenDetail = {
- type: this.type,
+ type,
year,
// Month value from input box starts from 1 instead of 0
month: month == undefined ? undefined : month - 1,
@@ -115,7 +98,7 @@ export class DateTimePickerPanel {
stepBase: detail.stepBase,
};
- if (this.type !== "time") {
+ if (type !== "time") {
const { firstDayOfWeek, weekends } = this.getCalendarInfo(locale);
const monthDisplayNames = new Services.intl.DisplayNames(locale, {
@@ -143,59 +126,33 @@ export class DateTimePickerPanel {
weekdayStrings,
});
}
- this.postMessageToPicker({
- name: "PickerInit",
- detail: flattenDetail,
- });
+ return flattenDetail;
}
- setInputBoxValue() {
- const value = {
- year: this.pickerState.year,
- month: this.pickerState.month,
- day: this.pickerState.day,
- hour: this.pickerState.hour,
- minute: this.pickerState.minute,
- };
- this.sendPickerValueChanged(value);
- }
-
- sendPickerValueChanged(value) {
- let detail = {};
- switch (this.type) {
+ /**
+ * Input element state updater function called when the picker value is changed
+ *
+ * @param {string} type
+ * @param {object} pickerState
+ */
+ sendPickerValueChangedImpl(type, pickerState) {
+ let { year, month, day, hour, minute } = pickerState;
+ if (month !== undefined) {
+ // Month value from input box starts from 1 instead of 0
+ month += 1;
+ }
+ switch (type) {
case "time": {
- detail = {
- hour: value.hour,
- minute: value.minute,
- };
- break;
+ return { hour, minute };
}
case "date": {
- 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;
+ return { year, month, day };
}
case "datetime-local": {
- 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,
- hour: value.hour,
- minute: value.minute,
- };
- break;
+ return { year, month, day, hour, minute };
}
}
- this.element.dispatchEvent(
- new CustomEvent("DateTimePickerValueChanged", {
- detail,
- })
- );
+ throw new Error(`Unexpected type ${type}`);
}
getCalendarInfo(locale) {
@@ -218,46 +175,4 @@ export class DateTimePickerPanel {
weekends,
};
}
-
- 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(aEvent.data.detail);
- break;
- }
- }
- }
-
- postMessageToPicker(data) {
- if (
- this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
- ) {
- this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
- }
- }
}
diff --git a/toolkit/modules/InputPickerPanelCommon.sys.mjs b/toolkit/modules/InputPickerPanelCommon.sys.mjs
@@ -0,0 +1,166 @@
+/* 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/. */
+
+export class InputPickerPanelCommon {
+ #element;
+ #filename;
+ #pickerState;
+ #type;
+ #detail;
+ /** @type {AbortController} */
+ #abortController;
+
+ /**
+ * @param {XULElement} element
+ * @param {string} filename
+ */
+ constructor(element, filename) {
+ this.#element = element;
+ this.#filename = filename;
+ }
+
+ get #popupFrame() {
+ const id = `inputPickerPopupFrame`;
+ let frame = this.#element.ownerDocument.getElementById(id);
+ if (!frame) {
+ frame = this.#element.ownerDocument.createXULElement("iframe");
+ frame.id = id;
+ this.#element.appendChild(frame);
+ }
+ return frame;
+ }
+
+ openPicker(type, rect, detail) {
+ const impl = this.openPickerImpl(type);
+ this.#pickerState = {};
+ // TODO: Resize picker according to content zoom level
+ this.#element.style.fontSize = "10px";
+ this.#type = impl.type;
+ this.#detail = detail;
+ this.#abortController = new AbortController();
+ this.#popupFrame.addEventListener("load", this, {
+ capture: true,
+ signal: this.#abortController.signal,
+ });
+ this.#popupFrame.setAttribute(
+ "src",
+ `chrome://global/content/${this.#filename}`
+ );
+ this.#popupFrame.style.width = impl.width;
+ this.#popupFrame.style.height = impl.height;
+ this.#element.openPopupAtScreenRect(
+ "after_start",
+ rect.left,
+ rect.top,
+ rect.width,
+ rect.height,
+ false,
+ false
+ );
+ }
+
+ /**
+ * @typedef {object} OpenPickerInfo
+ * @property {string} type The picker type
+ * @property {string} width The picker width in CSS value
+ * @property {string} height The picker height in CSS value
+ *
+ * Picker window initialization function called when opening the picker
+ *
+ * @param {string} _type The input element type
+ * @returns {OpenPickerInfo}
+ */
+ openPickerImpl(_type) {
+ throw new Error("Not implemented");
+ }
+
+ closePicker(clear) {
+ if (clear) {
+ this.#element.dispatchEvent(new CustomEvent(`InputPickerValueCleared`));
+ }
+ this.#pickerState = {};
+ this.#type = undefined;
+ this.#abortController.abort();
+ this.#popupFrame.setAttribute("src", "");
+ this.#element.hidePopup();
+ }
+
+ initPicker(detail) {
+ const implDetail = this.initPickerImpl(this.#type, detail);
+ this.postMessageToPicker({
+ name: "PickerInit",
+ detail: implDetail,
+ });
+ }
+
+ /**
+ * Popup frame initialization function called when the picker window is loaded
+ *
+ * @param {string} _type The picker type
+ * @param {object} _detail The argument from the child actor's openPickerImpl
+ * @returns An argument object to pass to the popup frame
+ */
+ initPickerImpl(_type, _detail) {
+ throw new Error("Not implemented");
+ }
+
+ sendPickerValueChanged() {
+ let detail = this.sendPickerValueChangedImpl(this.#type, this.#pickerState);
+ this.#element.dispatchEvent(
+ new CustomEvent(`InputPickerValueChanged`, {
+ detail,
+ })
+ );
+ }
+
+ /**
+ * Input element state updater function called when the picker value is changed
+ *
+ * @param {string} _type
+ * @param {object} _pickerState
+ */
+ sendPickerValueChangedImpl(_type, _pickerState) {
+ throw new Error("Not implemented");
+ }
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "load": {
+ this.initPicker(this.#detail);
+ this.#popupFrame.contentWindow.addEventListener("message", this, {
+ signal: this.#abortController.signal,
+ });
+ break;
+ }
+ case "message": {
+ this.handleMessage(aEvent);
+ break;
+ }
+ }
+ }
+
+ handleMessage(aEvent) {
+ if (!this.#popupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
+ return;
+ }
+
+ switch (aEvent.data.name) {
+ case "PickerPopupChanged": {
+ this.#pickerState = aEvent.data.detail;
+ this.sendPickerValueChanged();
+ break;
+ }
+ case "ClosePopup": {
+ this.closePicker(aEvent.data.detail);
+ break;
+ }
+ }
+ }
+
+ postMessageToPicker(data) {
+ if (this.#popupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
+ this.#popupFrame.contentWindow.postMessage(data, "*");
+ }
+ }
+}
diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build
@@ -162,7 +162,6 @@ EXTRA_JS_MODULES += [
"Console.sys.mjs",
"ContentDOMReference.sys.mjs",
"CreditCard.sys.mjs",
- "DateTimePickerPanel.sys.mjs",
"DeferredTask.sys.mjs",
"E10SUtils.sys.mjs",
"EventEmitter.sys.mjs",
@@ -215,6 +214,11 @@ EXTRA_JS_MODULES += [
"WebChannel.sys.mjs",
]
+MOZ_SRC_FILES += [
+ "DateTimePickerPanel.sys.mjs",
+ "InputPickerPanelCommon.sys.mjs",
+]
+
if CONFIG["MOZ_ASAN_REPORTER"]:
EXTRA_JS_MODULES += [
"AsanReporter.sys.mjs",