Skip to content

Commit 73fb108

Browse files
Bug 1855045 - [bidi] Add support for input.fileDialogOpened event r=Sasha
Differential Revision: https://phabricator.services.mozilla.com/D271791
1 parent a99dfbc commit 73fb108

File tree

7 files changed

+280
-62
lines changed

7 files changed

+280
-62
lines changed

remote/jar.mn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ remote.jar:
5252
content/shared/listeners/ContextualIdentityListener.sys.mjs (shared/listeners/ContextualIdentityListener.sys.mjs)
5353
content/shared/listeners/DataChannelListener.sys.mjs (shared/listeners/DataChannelListener.sys.mjs)
5454
content/shared/listeners/DownloadListener.sys.mjs (shared/listeners/DownloadListener.sys.mjs)
55+
content/shared/listeners/FilePickerListener.sys.mjs (shared/listeners/FilePickerListener.sys.mjs)
5556
content/shared/listeners/LoadListener.sys.mjs (shared/listeners/LoadListener.sys.mjs)
5657
content/shared/listeners/NavigationListener.sys.mjs (shared/listeners/NavigationListener.sys.mjs)
5758
content/shared/listeners/NetworkEventRecord.sys.mjs (shared/listeners/NetworkEventRecord.sys.mjs)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
5+
const lazy = {};
6+
7+
ChromeUtils.defineESModuleGetters(lazy, {
8+
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
9+
});
10+
11+
const OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING = "file-input-picker-opening";
12+
13+
/**
14+
* The FilePickerListener can be used to listen for file picker dialog openings
15+
* triggered by input type=file elements.
16+
*
17+
* Note that the actual file picker might not open if it is automatically
18+
* dismissed as part of the defined user prompt behavior.
19+
*
20+
* Example:
21+
* ```
22+
* const listener = new FilePickerListener();
23+
* listener.on("file-picker-opening", onFilePickerOpened);
24+
* listener.startListening();
25+
*
26+
* const onFilePickerOpened = (eventName, data) => {
27+
* const { element } = data;
28+
* console.log("File picker opened:", element.multiple);
29+
* };
30+
* ```
31+
*
32+
* @fires FilePickerListener#"file-picker-opening"
33+
* The FilePickerListener emits the following event:
34+
* - "file-picker-opening" when a file picker is requested to be opened,
35+
* with the following object as payload:
36+
* - {Element} element
37+
* The DOM element which triggered the file picker to open.
38+
*/
39+
export class FilePickerListener {
40+
#listening;
41+
42+
constructor() {
43+
lazy.EventEmitter.decorate(this);
44+
45+
this.#listening = false;
46+
}
47+
48+
destroy() {
49+
this.stopListening();
50+
}
51+
52+
observe(subject, topic) {
53+
switch (topic) {
54+
case OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING: {
55+
this.emit("file-picker-opening", {
56+
element: subject,
57+
});
58+
break;
59+
}
60+
}
61+
}
62+
63+
startListening() {
64+
if (this.#listening) {
65+
return;
66+
}
67+
Services.obs.addObserver(this, OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING);
68+
this.#listening = true;
69+
}
70+
71+
stopListening() {
72+
if (!this.#listening) {
73+
return;
74+
}
75+
Services.obs.removeObserver(this, OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING);
76+
this.#listening = false;
77+
}
78+
}

remote/webdriver-bidi/jar.mn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ remote.jar:
4141

4242
# WebDriver BiDi windowglobal-in-root modules
4343
content/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs (modules/windowglobal-in-root/browsingContext.sys.mjs)
44+
content/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs (modules/windowglobal-in-root/input.sys.mjs)
4445
content/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs (modules/windowglobal-in-root/log.sys.mjs)
4546
content/webdriver-bidi/modules/windowglobal-in-root/network.sys.mjs (modules/windowglobal-in-root/network.sys.mjs)
4647
content/webdriver-bidi/modules/windowglobal-in-root/script.sys.mjs (modules/windowglobal-in-root/script.sys.mjs)

remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ ChromeUtils.defineESModuleGetters(modules.root, {
4141
ChromeUtils.defineESModuleGetters(modules["windowglobal-in-root"], {
4242
browsingContext:
4343
"chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs",
44+
input:
45+
"chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs",
4446
log: "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs",
4547
network:
4648
"chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/network.sys.mjs",

remote/webdriver-bidi/modules/root/input.sys.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ class InputModule extends RootBiDiModule {
395395
}
396396

397397
static get supportedEvents() {
398-
return [];
398+
return ["input.fileDialogOpened"];
399399
}
400400
}
401401

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
5+
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
6+
7+
const lazy = {};
8+
9+
ChromeUtils.defineESModuleGetters(lazy, {
10+
NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
11+
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
12+
});
13+
14+
class InputModule extends Module {
15+
destroy() {}
16+
17+
interceptEvent(name, payload) {
18+
if (name == "input.fileDialogOpened") {
19+
const browsingContext = payload.context;
20+
if (!lazy.TabManager.isValidCanonicalBrowsingContext(browsingContext)) {
21+
// Discard events for invalid browsing contexts.
22+
return null;
23+
}
24+
25+
// Resolve browsing context to a Navigable id.
26+
payload.context =
27+
lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
28+
}
29+
30+
return payload;
31+
}
32+
}
33+
34+
export const input = InputModule;

remote/webdriver-bidi/modules/windowglobal/input.sys.mjs

Lines changed: 163 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,97 @@ ChromeUtils.defineESModuleGetters(lazy, {
1515
dom: "chrome://remote/content/shared/DOM.sys.mjs",
1616
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
1717
event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
18+
FilePickerListener:
19+
"chrome://remote/content/shared/listeners/FilePickerListener.sys.mjs",
20+
OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
21+
setDefaultSerializationOptions:
22+
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
1823
});
1924

2025
class InputModule extends WindowGlobalBiDiModule {
26+
#filePickerListener;
27+
#subscribedEvents;
28+
2129
constructor(messageHandler) {
2230
super(messageHandler);
31+
32+
this.#filePickerListener = new lazy.FilePickerListener();
33+
this.#filePickerListener.on(
34+
"file-picker-opening",
35+
this.#onFilePickerOpening
36+
);
37+
38+
// Set of event names which have active subscriptions.
39+
this.#subscribedEvents = new Set();
40+
}
41+
42+
destroy() {
43+
this.#filePickerListener.off(
44+
"file-picker-opening",
45+
this.#onFilePickerOpening
46+
);
47+
this.#subscribedEvents = null;
2348
}
2449

25-
destroy() {}
50+
async setFiles(options) {
51+
const { element: sharedReference, files } = options;
52+
53+
const element =
54+
await this.#deserializeElementSharedReference(sharedReference);
55+
56+
if (
57+
!HTMLInputElement.isInstance(element) ||
58+
element.type !== "file" ||
59+
element.disabled
60+
) {
61+
throw new lazy.error.UnableToSetFileInputError(
62+
`Element needs to be an <input> element with type "file" and not disabled`
63+
);
64+
}
65+
66+
if (files.length > 1 && !element.hasAttribute("multiple")) {
67+
throw new lazy.error.UnableToSetFileInputError(
68+
`Element should have an attribute "multiple" set when trying to set more than 1 file`
69+
);
70+
}
71+
72+
const fileObjects = [];
73+
for (const file of files) {
74+
try {
75+
fileObjects.push(await File.createFromFileName(file));
76+
} catch (e) {
77+
throw new lazy.error.UnsupportedOperationError(
78+
`Failed to add file ${file} (${e})`
79+
);
80+
}
81+
}
82+
83+
const selectedFiles = Array.from(element.files);
84+
85+
const intersection = fileObjects.filter(fileObject =>
86+
selectedFiles.some(
87+
selectedFile =>
88+
// Compare file fields to identify if the files are equal.
89+
// TODO: Bug 1883856. Add check for full path or use a different way
90+
// to compare files when it's available.
91+
selectedFile.name === fileObject.name &&
92+
selectedFile.size === fileObject.size &&
93+
selectedFile.type === fileObject.type
94+
)
95+
);
96+
97+
if (
98+
intersection.length === selectedFiles.length &&
99+
selectedFiles.length === fileObjects.length
100+
) {
101+
lazy.event.cancel(element);
102+
} else {
103+
element.mozSetFileArray(fileObjects);
104+
105+
lazy.event.input(element);
106+
lazy.event.change(element);
107+
}
108+
}
26109

27110
async #deserializeElementSharedReference(sharedReference) {
28111
if (typeof sharedReference?.sharedId !== "string") {
@@ -43,6 +126,85 @@ class InputModule extends WindowGlobalBiDiModule {
43126
return element;
44127
}
45128

129+
#onFilePickerOpening = (eventName, data) => {
130+
const { element } = data;
131+
if (element.ownerGlobal.browsingContext != this.messageHandler.context) {
132+
return;
133+
}
134+
135+
const realm = this.messageHandler.getRealm();
136+
137+
const serializedNode = this.serialize(
138+
element,
139+
lazy.setDefaultSerializationOptions(),
140+
lazy.OwnershipModel.None,
141+
realm
142+
);
143+
144+
this.emitEvent("input.fileDialogOpened", {
145+
context: this.messageHandler.context,
146+
element: serializedNode,
147+
multiple: element.multiple,
148+
});
149+
};
150+
151+
#startListingOnFilePickerOpened() {
152+
if (!this.#subscribedEvents.has("script.FilePickerOpened")) {
153+
this.#filePickerListener.startListening();
154+
}
155+
}
156+
157+
#stopListingOnFilePickerOpened() {
158+
if (this.#subscribedEvents.has("script.FilePickerOpened")) {
159+
this.#filePickerListener.stopListening();
160+
}
161+
}
162+
163+
#subscribeEvent(event) {
164+
switch (event) {
165+
case "input.fileDialogOpened": {
166+
this.#startListingOnFilePickerOpened();
167+
this.#subscribedEvents.add(event);
168+
break;
169+
}
170+
}
171+
}
172+
173+
#unsubscribeEvent(event) {
174+
switch (event) {
175+
case "input.fileDialogOpened": {
176+
this.#stopListingOnFilePickerOpened();
177+
this.#subscribedEvents.delete(event);
178+
break;
179+
}
180+
}
181+
}
182+
183+
_applySessionData(params) {
184+
// TODO: Bug 1775231. Move this logic to a shared module or an abstract
185+
// class.
186+
const { category } = params;
187+
if (category === "event") {
188+
const filteredSessionData = params.sessionData.filter(item =>
189+
this.messageHandler.matchesContext(item.contextDescriptor)
190+
);
191+
for (const event of this.#subscribedEvents.values()) {
192+
const hasSessionItem = filteredSessionData.some(
193+
item => item.value === event
194+
);
195+
// If there are no session items for this context, we should unsubscribe from the event.
196+
if (!hasSessionItem) {
197+
this.#unsubscribeEvent(event);
198+
}
199+
}
200+
201+
// Subscribe to all events, which have an item in SessionData.
202+
for (const { value } of filteredSessionData) {
203+
this.#subscribeEvent(value);
204+
}
205+
}
206+
}
207+
46208
_assertInViewPort(options) {
47209
const { target } = options;
48210

@@ -169,66 +331,6 @@ class InputModule extends WindowGlobalBiDiModule {
169331

170332
return [val.x / dpr, val.y / dpr];
171333
}
172-
173-
async setFiles(options) {
174-
const { element: sharedReference, files } = options;
175-
176-
const element =
177-
await this.#deserializeElementSharedReference(sharedReference);
178-
179-
if (
180-
!HTMLInputElement.isInstance(element) ||
181-
element.type !== "file" ||
182-
element.disabled
183-
) {
184-
throw new lazy.error.UnableToSetFileInputError(
185-
`Element needs to be an <input> element with type "file" and not disabled`
186-
);
187-
}
188-
189-
if (files.length > 1 && !element.hasAttribute("multiple")) {
190-
throw new lazy.error.UnableToSetFileInputError(
191-
`Element should have an attribute "multiple" set when trying to set more than 1 file`
192-
);
193-
}
194-
195-
const fileObjects = [];
196-
for (const file of files) {
197-
try {
198-
fileObjects.push(await File.createFromFileName(file));
199-
} catch (e) {
200-
throw new lazy.error.UnsupportedOperationError(
201-
`Failed to add file ${file} (${e})`
202-
);
203-
}
204-
}
205-
206-
const selectedFiles = Array.from(element.files);
207-
208-
const intersection = fileObjects.filter(fileObject =>
209-
selectedFiles.some(
210-
selectedFile =>
211-
// Compare file fields to identify if the files are equal.
212-
// TODO: Bug 1883856. Add check for full path or use a different way
213-
// to compare files when it's available.
214-
selectedFile.name === fileObject.name &&
215-
selectedFile.size === fileObject.size &&
216-
selectedFile.type === fileObject.type
217-
)
218-
);
219-
220-
if (
221-
intersection.length === selectedFiles.length &&
222-
selectedFiles.length === fileObjects.length
223-
) {
224-
lazy.event.cancel(element);
225-
} else {
226-
element.mozSetFileArray(fileObjects);
227-
228-
lazy.event.input(element);
229-
lazy.event.change(element);
230-
}
231-
}
232334
}
233335

234336
export const input = InputModule;

0 commit comments

Comments
 (0)