@@ -6,47 +6,181 @@ const { ContentDOMReference } = ChromeUtils.importESModule(
66 "resource://gre/modules/ContentDOMReference.sys.mjs"
77) ;
88
9+ const { DOMUtils } = ChromeUtils . importESModule (
10+ "resource://gre/modules/DOMUtils.sys.mjs"
11+ ) ;
12+
13+ const { BrowserCustomizableShared } = ChromeUtils . importESModule (
14+ "resource://gre/modules/BrowserCustomizableShared.sys.mjs"
15+ ) ;
16+
17+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" ;
18+
919export class DotContextMenuChild extends JSWindowActorChild {
1020 /**
11- * @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument } message
21+ * Creates a new context object
22+ * @param {Event } event
23+ * @param {Node } target
1224 */
13- receiveMessage ( message ) {
14- const { targetIdentifier } = message . data ;
25+ createContext ( event , target ) {
26+ return { } ;
27+ }
28+
29+ /**
30+ * Fired when a context menu is requested via the contextmenu DOM event
31+ * @param {MouseEvent } event
32+ */
33+ #onContextMenuRequest( event ) {
34+ let { defaultPrevented } = event ;
35+
36+ const composedTarget = /** @type {Element } */ ( event . composedTarget ) ;
37+
38+ // Ignore contextmenu events on a chrome browser, as we'll
39+ // handle the contextmenu event from inside the child content.
40+ if (
41+ composedTarget . namespaceURI == XUL_NS &&
42+ composedTarget . tagName == "browser"
43+ )
44+ return ;
45+
46+ if (
47+ // If the event originated from a non-chrome document
48+ // and we have disabled the contextmenu event, ensure
49+ // our context menu cannot be prevented.
50+ ! composedTarget . nodePrincipal . isSystemPrincipal &&
51+ ! Services . prefs . getBoolPref ( "dom.event.contextmenu.enabled" )
52+ ) {
53+ defaultPrevented = false ;
54+ }
55+
56+ if ( defaultPrevented ) return ;
57+
58+ const context = this . createContext ( event , composedTarget ) ;
59+
60+ console . log ( "ChildSend" , composedTarget , context ) ;
61+
62+ this . sendAsyncMessage ( "contextmenu" , {
63+ target : ContentDOMReference . get ( composedTarget ) ,
64+
65+ screenX : event . screenX ,
66+ screenY : event . screenY ,
67+
68+ context
69+ } ) ;
70+ }
71+
72+ /**
73+ * Fetches the context menu targets for an event
74+ * @param {Event } event
75+ * @returns {Set<Element> }
76+ */
77+ #getEventContextMenuTargets( event ) {
78+ const eventBubblePath = /** @type {Element[] } */ ( event . composedPath ( ) ) ;
79+
80+ const contextMenuTargets = new Set ( ) ;
1581
16- switch ( message . name ) {
17- case "ContextMenu:ReloadFrame" : {
18- const { forceReload } = message . data ;
82+ for ( const node of eventBubblePath ) {
83+ // Checks if the node is actually an element
84+ if ( ! node || ! node . getAttribute ) continue ;
1985
20- const target = ContentDOMReference . resolve ( targetIdentifier ) ;
86+ // If we bubble through an element without a contextmenu,
87+ // continue to the next element in the bubble path.
88+ const contextMenuId = node . getAttribute ( "contextmenu" ) ;
89+ if ( ! contextMenuId ) continue ;
2190
22- /** @type {any } */ ( target . ownerDocument . location ) . reload (
23- forceReload
91+ // Checks whether it bubbles through a customizable area implementation
92+ const implementsContext =
93+ BrowserCustomizableShared . isCustomizableAreaImplementation (
94+ node
2495 ) ;
96+
97+ /** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement } */
98+ let contextMenu = null ;
99+
100+ if ( contextMenuId && contextMenuId . length ) {
101+ // if contextMenu == _child, look for first <menupopup> child
102+ if ( contextMenuId == "_child" ) {
103+ contextMenu = node . querySelector ( "menupopup" ) ;
104+ } else {
105+ const contextMenuEl =
106+ node . ownerDocument . getElementById ( contextMenuId ) ;
107+
108+ if ( contextMenuEl ) {
109+ if ( contextMenuEl . tagName == "menupopup" ) {
110+ contextMenu =
111+ /** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement } */ (
112+ contextMenuEl
113+ ) ;
114+ }
115+ }
116+ }
117+ }
118+
119+ if ( ! contextMenu ) continue ;
120+
121+ contextMenuTargets . add ( contextMenu ) ;
122+
123+ // If we hit a non-contextual element, like a button, stop iterating
124+ // as we cannot inherit any more items from further up in the bubble path.
125+ if ( ! implementsContext ) {
25126 break ;
26127 }
27128 }
129+
130+ return contextMenuTargets ;
28131 }
29132
30133 /**
31- * Creates a new context menu context object from an event
32- * @param {MouseEvent } event
134+ * Fired when a context menu is launched
135+ * @param {CustomEvent<{ context: Record<string, any>; screenX: number; screenY: number }> } event
33136 */
34- _createContext ( event ) {
35- return { } ;
137+ #onContextMenuLaunch( event ) {
138+ const { screenX, screenY } = event . detail ;
139+
140+ const target = /** @type {Node } */ ( event . composedTarget ) ;
141+
142+ const contextMenuTargets = this . #getEventContextMenuTargets( event ) ;
143+ if ( ! contextMenuTargets . size ) return ;
144+
145+ const contextMenuItems = Array . from ( contextMenuTargets . values ( ) )
146+ . map ( ( t ) => Array . from ( t . childNodes ) )
147+ . reduce ( ( prev , curr ) => ( prev || [ ] ) . concat ( curr ) )
148+ . map ( ( i ) => i . cloneNode ( true ) ) ;
149+
150+ const contextMenu =
151+ /** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement } */ (
152+ target . ownerDocument . getElementById (
153+ "constructed-context-menu"
154+ ) || target . ownerDocument . createXULElement ( "menupopup" )
155+ ) ;
156+
157+ contextMenu . id = "constructed-context-menu" ;
158+ contextMenu . replaceChildren ( ...contextMenuItems ) ;
159+
160+ if ( ! contextMenu . parentElement ) {
161+ target . ownerDocument
162+ . querySelector ( "popupset" )
163+ . appendChild ( contextMenu ) ;
164+ }
165+ contextMenu . openPopupAtScreen ( screenX , screenY , true , event ) ;
166+
167+ console . log ( "ChildReceive" , target , contextMenuItems ) ;
36168 }
37169
38170 /**
39- * Receives incoming contextmenu events
40- * @param {MouseEvent } event
171+ * Handles incoming events to the context menu child
172+ * @param {Event } event
41173 */
42- async handleEvent ( event ) {
43- const context = this . _createContext ( event ) ;
44-
45- this . sendAsyncMessage ( "contextmenu" , {
46- x : event . screenX ,
47- y : event . screenY ,
48-
49- context
50- } ) ;
174+ handleEvent ( event ) {
175+ switch ( event . type ) {
176+ case "contextmenu" : {
177+ this . #onContextMenuRequest( /** @type {MouseEvent } */ ( event ) ) ;
178+ break ;
179+ }
180+ case "ContextMenu::Launch" : {
181+ this . #onContextMenuLaunch( /** @type {CustomEvent } */ ( event ) ) ;
182+ break ;
183+ }
184+ }
51185 }
52186}
0 commit comments