summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/ClipboardContextMenu.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/ClipboardContextMenu.sys.mjs')
-rw-r--r--toolkit/modules/ClipboardContextMenu.sys.mjs214
1 files changed, 214 insertions, 0 deletions
diff --git a/toolkit/modules/ClipboardContextMenu.sys.mjs b/toolkit/modules/ClipboardContextMenu.sys.mjs
new file mode 100644
index 0000000000..011bfe64d7
--- /dev/null
+++ b/toolkit/modules/ClipboardContextMenu.sys.mjs
@@ -0,0 +1,214 @@
+/* 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, {
+ PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
+});
+
+export var ClipboardContextMenu = {
+ MENU_POPUP_ID: "clipboardReadPasteMenuPopup",
+
+ // EventListener interface.
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "command": {
+ this.onCommand();
+ break;
+ }
+ case "popuphiding": {
+ this.onPopupHiding();
+ break;
+ }
+ case "keydown": {
+ this.onKeyDown(aEvent);
+ break;
+ }
+ }
+ },
+
+ _pasteMenuItemClicked: false,
+
+ onCommand() {
+ // onPopupHiding is responsible for returning result by calling onComplete
+ // function.
+ this._pasteMenuItemClicked = true;
+ },
+
+ onPopupHiding() {
+ // Remove the listeners before potentially sending the async message
+ // below, because that might throw.
+ this._removeMenupopupEventListeners();
+ this._clearDelayTimer();
+ this._stopWatchingForSpammyActivation();
+
+ this._menupopup = null;
+ this._menuitem = null;
+
+ let propBag = lazy.PromptUtils.objectToPropBag({
+ ok: this._pasteMenuItemClicked,
+ });
+ this._pendingRequest.resolve(propBag);
+
+ // A result has already been responded to. Reset the state to properly
+ // handle further click or dismiss events.
+ this._pasteMenuItemClicked = false;
+ this._pendingRequest = null;
+ },
+
+ _lastBeepTime: 0,
+
+ onKeyDown(aEvent) {
+ if (!this._menuitem.disabled) {
+ return;
+ }
+
+ let accesskey = this._menuitem.getAttribute("accesskey");
+ if (
+ aEvent.key == accesskey.toLowerCase() ||
+ aEvent.key == accesskey.toUpperCase()
+ ) {
+ if (Date.now() - this._lastBeepTime > 1000) {
+ Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep();
+ this._lastBeepTime = Date.now();
+ }
+ this._refreshDelayTimer();
+ }
+ },
+
+ _menupopup: null,
+ _menuitem: null,
+ _pendingRequest: null,
+
+ confirmUserPaste(aWindowContext) {
+ return new Promise((resolve, reject) => {
+ if (!aWindowContext) {
+ reject(
+ Components.Exception("Null window context.", Cr.NS_ERROR_INVALID_ARG)
+ );
+ return;
+ }
+
+ let { document } = aWindowContext.browsingContext.topChromeWindow;
+ if (!document) {
+ reject(
+ Components.Exception(
+ "Unable to get chrome document.",
+ Cr.NS_ERROR_FAILURE
+ )
+ );
+ return;
+ }
+
+ if (this._pendingRequest) {
+ reject(
+ Components.Exception(
+ "There is an ongoing request.",
+ Cr.NS_ERROR_FAILURE
+ )
+ );
+ return;
+ }
+
+ this._pendingRequest = { resolve, reject };
+ this._menupopup = this._getMenupopup(document);
+ this._menuitem = this._menupopup.firstElementChild;
+ this._addMenupopupEventListeners();
+
+ let mouseXInCSSPixels = {};
+ let mouseYInCSSPixels = {};
+ document.ownerGlobal.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
+ mouseXInCSSPixels,
+ mouseYInCSSPixels
+ );
+
+ this._menuitem.disabled = true;
+ this._startWatchingForSpammyActivation();
+ // `openPopup` is a no-op if the popup is already opened.
+ // That property is used when `navigator.clipboard.readText()` or
+ // `navigator.clipboard.read()`is called from two different frames, e.g.
+ // an iframe and the top level frame. In that scenario, the two frames
+ // correspond to different `navigator.clipboard` instances. When
+ // `readText()` or `read()` is called from both frames, an actor pair is
+ // instantiated for each of them. Both actor parents will call `openPopup`
+ // on the same `_menupopup` object. If the popup is already open,
+ // `openPopup` is a no-op. When the popup is clicked or dismissed both
+ // actor parents will receive the corresponding event.
+ this._menupopup.openPopup(
+ null,
+ "overlap" /* options */,
+ mouseXInCSSPixels.value,
+ mouseYInCSSPixels.value,
+ true /* isContextMenu */
+ );
+
+ this._refreshDelayTimer(document);
+ });
+ },
+
+ _addMenupopupEventListeners() {
+ this._menupopup.addEventListener("command", this);
+ this._menupopup.addEventListener("popuphiding", this);
+ },
+
+ _removeMenupopupEventListeners() {
+ this._menupopup.removeEventListener("command", this);
+ this._menupopup.removeEventListener("popuphiding", this);
+ },
+
+ _createMenupopup(aChromeDoc) {
+ let menuitem = aChromeDoc.createXULElement("menuitem");
+ menuitem.id = "clipboardReadPasteMenuItem";
+ aChromeDoc.l10n.setAttributes(menuitem, "text-action-paste");
+
+ let menupopup = aChromeDoc.createXULElement("menupopup");
+ menupopup.id = this.MENU_POPUP_ID;
+ menupopup.appendChild(menuitem);
+ return menupopup;
+ },
+
+ _getMenupopup(aChromeDoc) {
+ let menupopup = aChromeDoc.getElementById(this.MENU_POPUP_ID);
+ if (menupopup == null) {
+ menupopup = this._createMenupopup(aChromeDoc);
+ const parent =
+ aChromeDoc.querySelector("popupset") || aChromeDoc.documentElement;
+ parent.appendChild(menupopup);
+ }
+
+ return menupopup;
+ },
+
+ _startWatchingForSpammyActivation() {
+ let doc = this._menuitem.ownerDocument;
+ Services.els.addSystemEventListener(doc, "keydown", this, true);
+ },
+
+ _stopWatchingForSpammyActivation() {
+ let doc = this._menuitem.ownerDocument;
+ Services.els.removeSystemEventListener(doc, "keydown", this, true);
+ },
+
+ _delayTimer: null,
+
+ _clearDelayTimer() {
+ if (this._delayTimer) {
+ let window = this._menuitem.ownerGlobal;
+ window.clearTimeout(this._delayTimer);
+ this._delayTimer = null;
+ }
+ },
+
+ _refreshDelayTimer() {
+ this._clearDelayTimer();
+
+ let window = this._menuitem.ownerGlobal;
+ let delay = Services.prefs.getIntPref("security.dialog_enable_delay");
+ this._delayTimer = window.setTimeout(() => {
+ this._menuitem.disabled = false;
+ this._stopWatchingForSpammyActivation();
+ this._delayTimer = null;
+ }, delay);
+ },
+};