Bug 1796551 [wpt PR 36570] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / toolkit / actors / ClipboardReadPasteParent.jsm
blobb04a359f26022e21fbbe287c1a3ab242918c12b4
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 var EXPORTED_SYMBOLS = ["ClipboardReadPasteParent"];
8 const kMenuPopupId = "clipboardReadPasteMenuPopup";
10 // Exchanges messages with the child actor and handles events from the
11 // pasteMenuHandler.
12 class ClipboardReadPasteParent extends JSWindowActorParent {
13   constructor() {
14     super();
16     this._menupopup = null;
17     this._menuitem = null;
18     this._delayTimer = null;
19     this._pasteMenuItemClicked = false;
20     this._lastBeepTime = 0;
21   }
23   didDestroy() {
24     if (this._menupopup) {
25       this._menupopup.hidePopup(true);
26     }
27   }
29   // EventListener interface.
30   handleEvent(aEvent) {
31     switch (aEvent.type) {
32       case "command": {
33         this.onCommand();
34         break;
35       }
36       case "popuphiding": {
37         this.onPopupHiding();
38         break;
39       }
40       case "keydown": {
41         this.onKeyDown(aEvent);
42         break;
43       }
44     }
45   }
47   onCommand() {
48     this._pasteMenuItemClicked = true;
49     this.sendAsyncMessage("ClipboardReadPaste:PasteMenuItemClicked");
50   }
52   onPopupHiding() {
53     // Remove the listeners before potentially sending the async message
54     // below, because that might throw.
55     this._removeMenupopupEventListeners();
56     this._clearDelayTimer();
57     this._stopWatchingForSpammyActivation();
59     if (this._pasteMenuItemClicked) {
60       // A message was already sent. Reset the state to handle further
61       // click/dismiss events properly.
62       this._pasteMenuItemClicked = false;
63     } else {
64       this.sendAsyncMessage("ClipboardReadPaste:PasteMenuItemDismissed");
65     }
66   }
68   onKeyDown(aEvent) {
69     if (!this._menuitem.disabled) {
70       return;
71     }
73     let accesskey = this._menuitem.getAttribute("accesskey");
74     if (
75       aEvent.key == accesskey.toLowerCase() ||
76       aEvent.key == accesskey.toUpperCase()
77     ) {
78       if (Date.now() - this._lastBeepTime > 1000) {
79         Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep();
80         this._lastBeepTime = Date.now();
81       }
82       this._refreshDelayTimer();
83     }
84   }
86   // For JSWindowActorParent.
87   receiveMessage(value) {
88     if (value.name == "ClipboardReadPaste:ShowMenupopup") {
89       if (!this._menupopup) {
90         this._menupopup = this._getMenupopup();
91         this._menuitem = this._menupopup.firstElementChild;
92       }
94       this._addMenupopupEventListeners();
96       const browser = this.browsingContext.top.embedderElement;
97       const window = browser.ownerGlobal;
98       const windowUtils = window.windowUtils;
100       let mouseXInCSSPixels = {};
101       let mouseYInCSSPixels = {};
102       windowUtils.getLastOverWindowPointerLocationInCSSPixels(
103         mouseXInCSSPixels,
104         mouseYInCSSPixels
105       );
107       this._menuitem.disabled = true;
108       this._startWatchingForSpammyActivation();
109       // `openPopup` is a no-op if the popup is already opened.
110       // That property is used when `navigator.clipboard.readText()` or
111       // `navigator.clipboard.read()`is called from two different frames, e.g.
112       // an iframe and the top level frame. In that scenario, the two frames
113       // correspond to different `navigator.clipboard` instances. When
114       // `readText()` or `read()` is called from both frames, an actor pair is
115       // instantiated for each of them. Both actor parents will call `openPopup`
116       // on the same `_menupopup` object. If the popup is already open,
117       // `openPopup` is a no-op. When the popup is clicked or dismissed both
118       // actor parents will receive the corresponding event.
119       this._menupopup.openPopup(
120         null,
121         "overlap" /* options */,
122         mouseXInCSSPixels.value,
123         mouseYInCSSPixels.value,
124         true /* isContextMenu */
125       );
127       this._refreshDelayTimer();
128     }
129   }
131   _addMenupopupEventListeners() {
132     this._menupopup.addEventListener("command", this);
133     this._menupopup.addEventListener("popuphiding", this);
134   }
136   _removeMenupopupEventListeners() {
137     this._menupopup.removeEventListener("command", this);
138     this._menupopup.removeEventListener("popuphiding", this);
139   }
141   _createMenupopup(aChromeDoc) {
142     let menuitem = aChromeDoc.createXULElement("menuitem");
143     menuitem.id = "clipboardReadPasteMenuItem";
144     menuitem.setAttribute("data-l10n-id", "text-action-paste");
146     let menupopup = aChromeDoc.createXULElement("menupopup");
147     menupopup.id = kMenuPopupId;
148     menupopup.appendChild(menuitem);
149     return menupopup;
150   }
152   _getMenupopup() {
153     let browser = this.browsingContext.top.embedderElement;
154     let window = browser.ownerGlobal;
155     let chromeDoc = window.document;
157     let menupopup = chromeDoc.getElementById(kMenuPopupId);
158     if (menupopup == null) {
159       menupopup = this._createMenupopup(chromeDoc);
160       const parent =
161         chromeDoc.querySelector("popupset") || chromeDoc.documentElement;
162       parent.appendChild(menupopup);
163     }
165     return menupopup;
166   }
168   _startWatchingForSpammyActivation() {
169     let doc = this._menuitem.ownerDocument;
170     Services.els.addSystemEventListener(doc, "keydown", this, true);
171   }
173   _stopWatchingForSpammyActivation() {
174     let doc = this._menuitem.ownerDocument;
175     Services.els.removeSystemEventListener(doc, "keydown", this, true);
176   }
178   _clearDelayTimer() {
179     if (this._delayTimer) {
180       let window = this._menuitem.ownerGlobal;
181       window.clearTimeout(this._delayTimer);
182       this._delayTimer = null;
183     }
184   }
186   _refreshDelayTimer() {
187     this._clearDelayTimer();
189     let window = this._menuitem.ownerGlobal;
190     let delay = Services.prefs.getIntPref("security.dialog_enable_delay");
191     this._delayTimer = window.setTimeout(() => {
192       this._menuitem.disabled = false;
193       this._stopWatchingForSpammyActivation();
194       this._delayTimer = null;
195     }, delay);
196   }