diff --git a/actors/DotContextMenuChild.sys.mjs b/actors/DotContextMenuChild.sys.mjs index 9056888fea..86ccf01165 100644 --- a/actors/DotContextMenuChild.sys.mjs +++ b/actors/DotContextMenuChild.sys.mjs @@ -6,47 +6,181 @@ const { ContentDOMReference } = ChromeUtils.importESModule( "resource://gre/modules/ContentDOMReference.sys.mjs" ); +const { DOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/DOMUtils.sys.mjs" +); + +const { BrowserCustomizableShared } = ChromeUtils.importESModule( + "resource://gre/modules/BrowserCustomizableShared.sys.mjs" +); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + export class DotContextMenuChild extends JSWindowActorChild { /** - * @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument} message + * Creates a new context object + * @param {Event} event + * @param {Node} target */ - receiveMessage(message) { - const { targetIdentifier } = message.data; + createContext(event, target) { + return {}; + } + + /** + * Fired when a context menu is requested via the contextmenu DOM event + * @param {MouseEvent} event + */ + #onContextMenuRequest(event) { + let { defaultPrevented } = event; + + const composedTarget = /** @type {Element} */ (event.composedTarget); + + // Ignore contextmenu events on a chrome browser, as we'll + // handle the contextmenu event from inside the child content. + if ( + composedTarget.namespaceURI == XUL_NS && + composedTarget.tagName == "browser" + ) + return; + + if ( + // If the event originated from a non-chrome document + // and we have disabled the contextmenu event, ensure + // our context menu cannot be prevented. + !composedTarget.nodePrincipal.isSystemPrincipal && + !Services.prefs.getBoolPref("dom.event.contextmenu.enabled") + ) { + defaultPrevented = false; + } + + if (defaultPrevented) return; + + const context = this.createContext(event, composedTarget); + + console.log("ChildSend", composedTarget, context); + + this.sendAsyncMessage("contextmenu", { + target: ContentDOMReference.get(composedTarget), + + screenX: event.screenX, + screenY: event.screenY, + + context + }); + } + + /** + * Fetches the context menu targets for an event + * @param {Event} event + * @returns {Set} + */ + #getEventContextMenuTargets(event) { + const eventBubblePath = /** @type {Element[]} */ (event.composedPath()); + + const contextMenuTargets = new Set(); - switch (message.name) { - case "ContextMenu:ReloadFrame": { - const { forceReload } = message.data; + for (const node of eventBubblePath) { + // Checks if the node is actually an element + if (!node || !node.getAttribute) continue; - const target = ContentDOMReference.resolve(targetIdentifier); + // If we bubble through an element without a contextmenu, + // continue to the next element in the bubble path. + const contextMenuId = node.getAttribute("contextmenu"); + if (!contextMenuId) continue; - /** @type {any} */ (target.ownerDocument.location).reload( - forceReload + // Checks whether it bubbles through a customizable area implementation + const implementsContext = + BrowserCustomizableShared.isCustomizableAreaImplementation( + node ); + + /** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */ + let contextMenu = null; + + if (contextMenuId && contextMenuId.length) { + // if contextMenu == _child, look for first child + if (contextMenuId == "_child") { + contextMenu = node.querySelector("menupopup"); + } else { + const contextMenuEl = + node.ownerDocument.getElementById(contextMenuId); + + if (contextMenuEl) { + if (contextMenuEl.tagName == "menupopup") { + contextMenu = + /** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */ ( + contextMenuEl + ); + } + } + } + } + + if (!contextMenu) continue; + + contextMenuTargets.add(contextMenu); + + // If we hit a non-contextual element, like a button, stop iterating + // as we cannot inherit any more items from further up in the bubble path. + if (!implementsContext) { break; } } + + return contextMenuTargets; } /** - * Creates a new context menu context object from an event - * @param {MouseEvent} event + * Fired when a context menu is launched + * @param {CustomEvent<{ context: Record; screenX: number; screenY: number }>} event */ - _createContext(event) { - return {}; + #onContextMenuLaunch(event) { + const { screenX, screenY } = event.detail; + + const target = /** @type {Node} */ (event.composedTarget); + + const contextMenuTargets = this.#getEventContextMenuTargets(event); + if (!contextMenuTargets.size) return; + + const contextMenuItems = Array.from(contextMenuTargets.values()) + .map((t) => Array.from(t.childNodes)) + .reduce((prev, curr) => (prev || []).concat(curr)) + .map((i) => i.cloneNode(true)); + + const contextMenu = + /** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */ ( + target.ownerDocument.getElementById( + "constructed-context-menu" + ) || target.ownerDocument.createXULElement("menupopup") + ); + + contextMenu.id = "constructed-context-menu"; + contextMenu.replaceChildren(...contextMenuItems); + + if (!contextMenu.parentElement) { + target.ownerDocument + .querySelector("popupset") + .appendChild(contextMenu); + } + contextMenu.openPopupAtScreen(screenX, screenY, true, event); + + console.log("ChildReceive", target, contextMenuItems); } /** - * Receives incoming contextmenu events - * @param {MouseEvent} event + * Handles incoming events to the context menu child + * @param {Event} event */ - async handleEvent(event) { - const context = this._createContext(event); - - this.sendAsyncMessage("contextmenu", { - x: event.screenX, - y: event.screenY, - - context - }); + handleEvent(event) { + switch (event.type) { + case "contextmenu": { + this.#onContextMenuRequest(/** @type {MouseEvent} */ (event)); + break; + } + case "ContextMenu::Launch": { + this.#onContextMenuLaunch(/** @type {CustomEvent} */ (event)); + break; + } + } } } diff --git a/actors/DotContextMenuParent.sys.mjs b/actors/DotContextMenuParent.sys.mjs index 3975c83149..24d04f2083 100644 --- a/actors/DotContextMenuParent.sys.mjs +++ b/actors/DotContextMenuParent.sys.mjs @@ -1,29 +1,43 @@ /* 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/. */ + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { ContentDOMReference } = ChromeUtils.importESModule( + "resource://gre/modules/ContentDOMReference.sys.mjs" +); export class DotContextMenuParent extends JSWindowActorParent { - /** @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument<>} message */ + /** + * Handles incoming messages to the context menu parent + * @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument} message + */ receiveMessage(message) { - if (message.name !== "contextmenu") return; + // Attempt to resolve the target in the event, + // otherwise, check if we're inside a browser, + // otehrwise, use the top chrome window. + const target = /** @type {Element} */ ( + ContentDOMReference.resolve(message.data.target) || + this.browsingContext.embedderElement || + this.browsingContext.topChromeWindow + ); - const browser = this.manager.rootFrameLoader.ownerElement; - const win = browser.ownerGlobal; + const win = target.ownerGlobal; + const doc = target.ownerDocument; - // Make sure this browser belongs to us before we open the panel - if (win.gDot && win.gDot.tabs.getTabForWebContents(browser)) { - const { x, y, context } = message.data; + console.log("ParentReceive", target, message.data.context); - console.log(x, y, context); - } - } + const menuEvent = new /** @type {any} */ (win).CustomEvent( + "ContextMenu::Launch", + { + detail: message.data, + composed: true, + bubbles: true + } + ); + + console.log("ParentSend", target, menuEvent); + + target.dispatchEvent(menuEvent); - hiding() { - try { - this.sendAsyncMessage("ContextMenu:Hiding", {}); - } catch (e) { - // This will throw if the content goes away while the - // context menu is still open. - } } } diff --git a/third_party/dothq/gecko-types/index.d.ts b/third_party/dothq/gecko-types/index.d.ts index 18726aaa67..a62568905f 100755 --- a/third_party/dothq/gecko-types/index.d.ts +++ b/third_party/dothq/gecko-types/index.d.ts @@ -168,6 +168,8 @@ declare global { pressure: number, inputSource: number ) => void; + + clickEventPrevented: () => boolean; } interface XULElementWithCommandHandler { @@ -190,6 +192,10 @@ declare global { InspectorUtils: Gecko.InspectorUtils; windowGlobalChild: Gecko.WindowGlobalChildInstance; windowUtils: Gecko.WindowUtils; + scrollMaxX: number; + scrollMaxY: number; + scrollMinX: number; + scrollMinY: number; } interface Element extends Gecko.CustomElement { diff --git a/third_party/dothq/gecko-types/lib/BrowsingContext.d.ts b/third_party/dothq/gecko-types/lib/BrowsingContext.d.ts index 569c39e0e6..da6bf608fd 100644 --- a/third_party/dothq/gecko-types/lib/BrowsingContext.d.ts +++ b/third_party/dothq/gecko-types/lib/BrowsingContext.d.ts @@ -50,4 +50,6 @@ export interface BrowsingContext { currentWindowGlobal: WindowGlobalParent; group: BrowsingContextGroup; + + topChromeWindow?: ChromeWindow; } \ No newline at end of file diff --git a/third_party/dothq/gecko-types/lib/WindowUtils.d.ts b/third_party/dothq/gecko-types/lib/WindowUtils.d.ts index c591270ab6..e1cc66fc5a 100644 --- a/third_party/dothq/gecko-types/lib/WindowUtils.d.ts +++ b/third_party/dothq/gecko-types/lib/WindowUtils.d.ts @@ -20,11 +20,11 @@ export interface WindowUtils { height: number ): DOMRect; - /** - * Transform a rectangle given in coordinates relative to this document - * into CSS coordinates relative to the screen. - */ - toScreenRectInCSSUnits( + /** + * Transform a rectangle given in coordinates relative to this document + * into CSS coordinates relative to the screen. + */ + toScreenRectInCSSUnits( x: number, y: number, width: number, @@ -48,24 +48,34 @@ export interface WindowUtils { */ getBoundsWithoutFlushing(element: Element): DOMRect; + AGENT_SHEET: 0; + USER_SHEET: 1; + AUTHOR_SHEET: 2; + /** + * Synchronously loads a style sheet from |sheetURI| and adds it to the list + * of additional style sheets of the document. + * + * These additional style sheets are very much like user/agent sheets loaded + * with loadAndRegisterSheet. The only difference is that they are applied only + * on the document owned by this window. + * + * Sheets added via this API take effect immediately on the document. + */ + loadSheet(sheetURI: nsIURI, type: number): void; + + /** + * Same as the above method but allows passing the URI as a string. + */ + loadSheetUsingURIString(sheetURI: string, type: number): void; - AGENT_SHEET: 0; - USER_SHEET: 1; - AUTHOR_SHEET: 2; - /** - * Synchronously loads a style sheet from |sheetURI| and adds it to the list - * of additional style sheets of the document. - * - * These additional style sheets are very much like user/agent sheets loaded - * with loadAndRegisterSheet. The only difference is that they are applied only - * on the document owned by this window. - * - * Sheets added via this API take effect immediately on the document. - */ - loadSheet(sheetURI: nsIURI, type: number): void; - - /** - * Same as the above method but allows passing the URI as a string. - */ - loadSheetUsingURIString(sheetURI: string, type: number): void; + /** + * Sets WidgetEvent::mFlags::mOnlyChromeDispatch to true to ensure that + * the event is propagated only to chrome. + * Event's .target property will be aTarget. + * Returns the same value as what EventTarget.dispatchEvent does. + */ + dispatchEventToChromeOnly( + target: EventTarget, + event: Event + ): boolean; } diff --git a/types.d.ts b/types.d.ts index 80c86b1ef7..9a62bc7507 100644 --- a/types.d.ts +++ b/types.d.ts @@ -110,6 +110,7 @@ declare global { interface Event { defaultCancelled: boolean; defaultPreventedByChrome: boolean; + composedTarget: EventTarget; } interface Console { @@ -169,4 +170,8 @@ declare global { scrollLeftMin: number; scrollLeftMax: number; } + + interface Node { + nodePrincipal: any /** @todo: nsIPrincipal */; + } }