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