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/. */
7 loader.lazyRequireGetter(
10 "resource://devtools/shared/event-emitter.js"
14 * Client-side NodePicker module.
15 * To be used by inspector front when it needs to select DOM elements.
17 * NodePicker is a proxy for the node picker functionality from WalkerFront instances
18 * of all available InspectorFronts. It is a single point of entry for the client to:
19 * - invoke actions to start and stop picking nodes on all walkers
20 * - listen to node picker events from all walkers and relay them to subscribers
23 * @param {Commands} commands
24 * The commands object with all interfaces defined from devtools/shared/commands/
26 class NodePicker extends EventEmitter {
27 constructor(commands) {
29 this.commands = commands;
30 this.targetCommand = commands.targetCommand;
32 // Whether or not the node picker is active.
33 this.isPicking = false;
34 // Whether to focus the top-level frame before picking nodes.
38 // The set of inspector fronts corresponding to the targets where picking happens.
39 #currentInspectorFronts = new Set();
42 * Start/stop the element picker on the debuggee target.
44 * @param {Boolean} doFocus
45 * Optionally focus the content area once the picker is activated.
46 * @return Promise that resolves when done
48 togglePicker = doFocus => {
50 return this.stop({ canceled: true });
52 return this.start(doFocus);
56 * This DOCUMENT_EVENT resource callback is only used for webextension targets
57 * to workaround the fact that some navigations will not create/destroy any
58 * target (eg when jumping from a background document to a popup document).
60 #onWebExtensionDocumentEventAvailable = async resources => {
61 const { DOCUMENT_EVENT } = this.commands.resourceCommand.TYPES;
63 for (const resource of resources) {
65 resource.resourceType == DOCUMENT_EVENT &&
66 resource.name === "dom-complete" &&
67 resource.targetFront.isTopLevel &&
68 // When switching frames for a webextension target, a first dom-complete
69 // resource is emitted when we start watching the new docshell, in the
70 // WindowGlobalTargetActor progress listener.
72 // However here, we are expecting the "fake" dom-complete resource
73 // emitted specifically from the webextension target actor, when the
74 // new docshell is finally recognized to be linked to the target's
75 // webextension. This resource is emitted from `_changeTopLevelDocument`
76 // and is the only one which will have `isFrameSwitching` set to true.
78 // It also emitted after the one for the new docshell, so to avoid
79 // stopping and starting the node-picker twice, we filter out the first
80 // resource, which does not have `isFrameSwitching` set.
81 resource.isFrameSwitching
83 const inspectorFront = await resource.targetFront.getFront("inspector");
84 // When a webextension target navigates, it will typically be between
85 // documents which are not under the same root (fallback-document,
86 // devtools-panel, popup). Even though we are not switching targets, we
87 // need to restart the node picker.
88 await inspectorFront.walker.cancelPick();
89 await inspectorFront.walker.pick(this.doFocus);
90 this.emitForTests("node-picker-webextension-target-restarted");
96 * Tell the walker front corresponding to the given inspector front to enter node
97 * picking mode (listen for mouse movements over its nodes) and set event listeners
98 * associated with node picking: hover node, pick node, preview, cancel. See WalkerSpec.
100 * @param {InspectorFront} inspectorFront
103 #onInspectorFrontAvailable = async inspectorFront => {
104 this.#currentInspectorFronts.add(inspectorFront);
105 // watchFront may notify us about inspector fronts that aren't initialized yet,
106 // so ensure waiting for initialization in order to have a defined `walker` attribute.
107 await inspectorFront.initialize();
108 const { walker } = inspectorFront;
109 walker.on("picker-node-hovered", this.#onHovered);
110 walker.on("picker-node-picked", this.#onPicked);
111 walker.on("picker-node-previewed", this.#onPreviewed);
112 walker.on("picker-node-canceled", this.#onCanceled);
113 await walker.pick(this.doFocus);
115 this.emitForTests("inspector-front-ready-for-picker", walker);
119 * Tell the walker front corresponding to the given inspector front to exit the node
120 * picking mode and remove all event listeners associated with node picking.
122 * @param {InspectorFront} inspectorFront
123 * @param {Boolean} isDestroyCodePath
124 * Optional. If true, we assume that's when the toolbox closes
125 * and we should avoid doing any RDP request.
128 #onInspectorFrontDestroyed = async (
130 { isDestroyCodepath } = {}
132 this.#currentInspectorFronts.delete(inspectorFront);
134 const { walker } = inspectorFront;
139 walker.off("picker-node-hovered", this.#onHovered);
140 walker.off("picker-node-picked", this.#onPicked);
141 walker.off("picker-node-previewed", this.#onPreviewed);
142 walker.off("picker-node-canceled", this.#onCanceled);
143 // Only do a RDP request if we stop the node picker from a user action.
144 // Avoid doing one when we close the toolbox, in this scenario
145 // the walker actor on the server side will automatically cancel the node picking.
146 if (!isDestroyCodepath) {
147 await walker.cancelPick();
152 * While node picking, we want each target's walker fronts to listen for mouse
153 * movements over their nodes and emit events. Walker fronts are obtained from
154 * inspector fronts so we watch for the creation and destruction of inspector fronts
155 * in order to add or remove the necessary event listeners.
157 * @param {TargetFront} targetFront
160 #onTargetAvailable = async ({ targetFront }) => {
161 targetFront.watchFronts(
163 this.#onInspectorFrontAvailable,
164 this.#onInspectorFrontDestroyed
169 * Start the element picker.
170 * This will instruct walker fronts of all available targets (and those of targets
171 * created while node picking is active) to listen for mouse movements over their nodes
172 * and trigger events when a node is hovered or picked.
174 * @param {Boolean} doFocus
175 * Optionally focus the content area once the picker is activated.
177 start = async doFocus => {
178 if (this.isPicking) {
181 this.isPicking = true;
182 this.doFocus = doFocus;
184 this.emit("picker-starting");
186 this.targetCommand.watchTargets({
187 types: this.targetCommand.ALL_TYPES,
188 onAvailable: this.#onTargetAvailable,
191 if (this.targetCommand.descriptorFront.isWebExtension) {
192 await this.commands.resourceCommand.watchResources(
193 [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
195 onAvailable: this.#onWebExtensionDocumentEventAvailable,
200 this.emit("picker-started");
204 * Stop the element picker. Note that the picker is automatically stopped when
205 * an element is picked.
207 * @param {Boolean} isDestroyCodePath
208 * Optional. If true, we assume that's when the toolbox closes
209 * and we should avoid doing any RDP request.
210 * @param {Boolean} canceled
211 * Optional. If true, emit an additional event to notify that the
212 * picker was canceled, ie stopped without selecting a node.
214 stop = async ({ isDestroyCodepath, canceled } = {}) => {
215 if (!this.isPicking) {
218 this.isPicking = false;
219 this.doFocus = false;
221 this.targetCommand.unwatchTargets({
222 types: this.targetCommand.ALL_TYPES,
223 onAvailable: this.#onTargetAvailable,
226 if (this.targetCommand.descriptorFront.isWebExtension) {
227 this.commands.resourceCommand.unwatchResources(
228 [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
230 onAvailable: this.#onWebExtensionDocumentEventAvailable,
236 for (const inspectorFront of this.#currentInspectorFronts) {
238 this.#onInspectorFrontDestroyed(inspectorFront, {
243 await Promise.all(promises);
245 this.#currentInspectorFronts.clear();
247 this.emit("picker-stopped");
250 this.emit("picker-node-canceled");
255 // Do not await for stop as the isDestroy argument will make this method synchronous
256 // and we want to avoid having an async destroy
257 this.stop({ isDestroyCodepath: true });
258 this.targetCommand = null;
259 this.commands = null;
263 * When a node is hovered by the mouse when the highlighter is in picker mode
265 * @param {Object} data
266 * Information about the node being hovered
268 #onHovered = data => {
269 this.emit("picker-node-hovered", data.node);
271 // We're going to cleanup references for all the other walkers, so that if we hover
272 // back the same node, we will receive a new `picker-node-hovered` event.
273 for (const inspectorFront of this.#currentInspectorFronts) {
274 if (inspectorFront.walker !== data.node.walkerFront) {
275 inspectorFront.walker.clearPicker();
281 * When a node has been picked while the highlighter is in picker mode
283 * @param {Object} data
284 * Information about the picked node
286 #onPicked = data => {
287 this.emit("picker-node-picked", data.node);
292 * When a node has been shift-clicked (previewed) while the highlighter is in
295 * @param {Object} data
296 * Information about the picked node
298 #onPreviewed = data => {
299 this.emit("picker-node-previewed", data.node);
303 * When the picker is canceled, stop the picker, and make sure the toolbox
306 #onCanceled = () => {
307 return this.stop({ canceled: true });
311 module.exports = NodePicker;