@@ -6,47 +6,181 @@ const { ContentDOMReference } = ChromeUtils.importESModule(
6
6
"resource://gre/modules/ContentDOMReference.sys.mjs"
7
7
) ;
8
8
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
+
9
19
export class DotContextMenuChild extends JSWindowActorChild {
10
20
/**
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
12
24
*/
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 ( ) ;
15
81
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 ;
19
85
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 ;
21
90
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
24
95
) ;
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 ) {
25
126
break ;
26
127
}
27
128
}
129
+
130
+ return contextMenuTargets ;
28
131
}
29
132
30
133
/**
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
33
136
*/
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 ) ;
36
168
}
37
169
38
170
/**
39
- * Receives incoming contextmenu events
40
- * @param {MouseEvent } event
171
+ * Handles incoming events to the context menu child
172
+ * @param {Event } event
41
173
*/
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
+ }
51
185
}
52
186
}
0 commit comments