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/. */
6 ChromeUtils.defineESModuleGetters(lazy, {
7 PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
10 export var ClipboardContextMenu = {
11 MENU_POPUP_ID: "clipboardReadPasteMenuPopup",
13 // EventListener interface.
15 switch (aEvent.type) {
25 this.onKeyDown(aEvent);
31 _pasteMenuItemClicked: false,
34 // onPopupHiding is responsible for returning result by calling onComplete
36 this._pasteMenuItemClicked = true;
40 // Remove the listeners before potentially sending the async message
41 // below, because that might throw.
42 this._removeMenupopupEventListeners();
43 this._clearDelayTimer();
44 this._stopWatchingForSpammyActivation();
46 this._menupopup = null;
47 this._menuitem = null;
49 let propBag = lazy.PromptUtils.objectToPropBag({
50 ok: this._pasteMenuItemClicked,
52 this._pendingRequest.resolve(propBag);
54 // A result has already been responded to. Reset the state to properly
55 // handle further click or dismiss events.
56 this._pasteMenuItemClicked = false;
57 this._pendingRequest = null;
63 if (!this._menuitem.disabled) {
67 let accesskey = this._menuitem.getAttribute("accesskey");
69 aEvent.key == accesskey.toLowerCase() ||
70 aEvent.key == accesskey.toUpperCase()
72 if (Date.now() - this._lastBeepTime > 1000) {
73 Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep();
74 this._lastBeepTime = Date.now();
76 this._refreshDelayTimer();
82 _pendingRequest: null,
84 confirmUserPaste(aWindowContext) {
85 return new Promise((resolve, reject) => {
86 if (!aWindowContext) {
88 Components.Exception("Null window context.", Cr.NS_ERROR_INVALID_ARG)
93 let { document } = aWindowContext.browsingContext.topChromeWindow;
97 "Unable to get chrome document.",
104 if (this._pendingRequest) {
106 Components.Exception(
107 "There is an ongoing request.",
114 this._pendingRequest = { resolve, reject };
115 this._menupopup = this._getMenupopup(document);
116 this._menuitem = this._menupopup.firstElementChild;
117 this._addMenupopupEventListeners();
119 let mouseXInCSSPixels = {};
120 let mouseYInCSSPixels = {};
121 document.ownerGlobal.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
126 this._menuitem.disabled = true;
127 this._startWatchingForSpammyActivation();
128 // `openPopup` is a no-op if the popup is already opened.
129 // That property is used when `navigator.clipboard.readText()` or
130 // `navigator.clipboard.read()`is called from two different frames, e.g.
131 // an iframe and the top level frame. In that scenario, the two frames
132 // correspond to different `navigator.clipboard` instances. When
133 // `readText()` or `read()` is called from both frames, an actor pair is
134 // instantiated for each of them. Both actor parents will call `openPopup`
135 // on the same `_menupopup` object. If the popup is already open,
136 // `openPopup` is a no-op. When the popup is clicked or dismissed both
137 // actor parents will receive the corresponding event.
138 this._menupopup.openPopup(
140 "overlap" /* options */,
141 mouseXInCSSPixels.value,
142 mouseYInCSSPixels.value,
143 true /* isContextMenu */
146 this._refreshDelayTimer(document);
150 _addMenupopupEventListeners() {
151 this._menupopup.addEventListener("command", this);
152 this._menupopup.addEventListener("popuphiding", this);
155 _removeMenupopupEventListeners() {
156 this._menupopup.removeEventListener("command", this);
157 this._menupopup.removeEventListener("popuphiding", this);
160 _createMenupopup(aChromeDoc) {
161 let menuitem = aChromeDoc.createXULElement("menuitem");
162 menuitem.id = "clipboardReadPasteMenuItem";
163 aChromeDoc.l10n.setAttributes(menuitem, "text-action-paste");
165 let menupopup = aChromeDoc.createXULElement("menupopup");
166 menupopup.id = this.MENU_POPUP_ID;
167 menupopup.appendChild(menuitem);
171 _getMenupopup(aChromeDoc) {
172 let menupopup = aChromeDoc.getElementById(this.MENU_POPUP_ID);
173 if (menupopup == null) {
174 menupopup = this._createMenupopup(aChromeDoc);
176 aChromeDoc.querySelector("popupset") || aChromeDoc.documentElement;
177 parent.appendChild(menupopup);
183 _startWatchingForSpammyActivation() {
184 let doc = this._menuitem.ownerDocument;
185 doc.addEventListener("keydown", this, {
187 mozSystemGroup: true,
191 _stopWatchingForSpammyActivation() {
192 let doc = this._menuitem.ownerDocument;
193 doc.removeEventListener("keydown", this, {
195 mozSystemGroup: true,
202 if (this._delayTimer) {
203 let window = this._menuitem.ownerGlobal;
204 window.clearTimeout(this._delayTimer);
205 this._delayTimer = null;
209 _refreshDelayTimer() {
210 this._clearDelayTimer();
212 let window = this._menuitem.ownerGlobal;
213 let delay = Services.prefs.getIntPref("security.dialog_enable_delay");
214 this._delayTimer = window.setTimeout(() => {
215 this._menuitem.disabled = false;
216 this._stopWatchingForSpammyActivation();
217 this._delayTimer = null;