Source code

Revision control

Copy as Markdown

Other Tools

/* 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,
});
}
}