node-picker.js (8981B)
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 "use strict"; 6 7 loader.lazyRequireGetter( 8 this, 9 "EventEmitter", 10 "resource://devtools/shared/event-emitter.js" 11 ); 12 13 /** 14 * Client-side NodePicker module. 15 * To be used by inspector front when it needs to select DOM elements. 16 * 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 21 * 22 * @param {Commands} commands 23 * The commands object with all interfaces defined from devtools/shared/commands/ 24 */ 25 class NodePicker extends EventEmitter { 26 constructor(commands) { 27 super(); 28 this.commands = commands; 29 this.targetCommand = commands.targetCommand; 30 31 // Whether or not the node picker is active. 32 this.isPicking = false; 33 // Whether to focus the top-level frame before picking nodes. 34 this.doFocus = false; 35 } 36 37 // The set of inspector fronts corresponding to the targets where picking happens. 38 #currentInspectorFronts = new Set(); 39 40 /** 41 * Start/stop the element picker on the debuggee target. 42 * 43 * @param {boolean} doFocus 44 * Optionally focus the content area once the picker is activated. 45 * @return Promise that resolves when done 46 */ 47 togglePicker = doFocus => { 48 if (this.isPicking) { 49 return this.stop({ canceled: true }); 50 } 51 return this.start(doFocus); 52 }; 53 54 /** 55 * Tell the walker front corresponding to the given inspector front to enter node 56 * picking mode (listen for mouse movements over its nodes) and set event listeners 57 * associated with node picking: hover node, pick node, preview, cancel. See WalkerSpec. 58 * 59 * @param {InspectorFront} inspectorFront 60 * @return {Promise} 61 */ 62 #onInspectorFrontAvailable = async inspectorFront => { 63 this.#currentInspectorFronts.add(inspectorFront); 64 // watchFront may notify us about inspector fronts that aren't initialized yet, 65 // so ensure waiting for initialization in order to have a defined `walker` attribute. 66 await inspectorFront.initialize(); 67 const { walker } = inspectorFront; 68 walker.on("picker-node-hovered", this.#onHovered); 69 walker.on("picker-node-picked", this.#onPicked); 70 walker.on("picker-node-previewed", this.#onPreviewed); 71 walker.on("picker-node-canceled", this.#onCanceled); 72 await walker.pick(this.doFocus); 73 74 this.emitForTests("inspector-front-ready-for-picker", walker); 75 }; 76 77 /** 78 * Tell the walker front corresponding to the given inspector front to exit the node 79 * picking mode and remove all event listeners associated with node picking. 80 * 81 * @param {InspectorFront} inspectorFront 82 * @param {boolean} isDestroyCodePath 83 * Optional. If true, we assume that's when the toolbox closes 84 * and we should avoid doing any RDP request. 85 * @return {Promise} 86 */ 87 #onInspectorFrontDestroyed = async ( 88 inspectorFront, 89 { isDestroyCodepath } = {} 90 ) => { 91 this.#currentInspectorFronts.delete(inspectorFront); 92 93 const { walker } = inspectorFront; 94 if (!walker) { 95 return; 96 } 97 98 walker.off("picker-node-hovered", this.#onHovered); 99 walker.off("picker-node-picked", this.#onPicked); 100 walker.off("picker-node-previewed", this.#onPreviewed); 101 walker.off("picker-node-canceled", this.#onCanceled); 102 // Only do a RDP request if we stop the node picker from a user action. 103 // Avoid doing one when we close the toolbox, in this scenario 104 // the walker actor on the server side will automatically cancel the node picking. 105 if (!isDestroyCodepath) { 106 await walker.cancelPick(); 107 } 108 }; 109 110 /** 111 * While node picking, we want each target's walker fronts to listen for mouse 112 * movements over their nodes and emit events. Walker fronts are obtained from 113 * inspector fronts so we watch for the creation and destruction of inspector fronts 114 * in order to add or remove the necessary event listeners. 115 * 116 * @param {TargetFront} targetFront 117 * @return {Promise} 118 */ 119 #onTargetAvailable = async ({ targetFront }) => { 120 targetFront.watchFronts( 121 "inspector", 122 this.#onInspectorFrontAvailable, 123 this.#onInspectorFrontDestroyed 124 ); 125 }; 126 127 /** 128 * Start the element picker. 129 * This will instruct walker fronts of all available targets (and those of targets 130 * created while node picking is active) to listen for mouse movements over their nodes 131 * and trigger events when a node is hovered or picked. 132 * 133 * @param {boolean} doFocus 134 * Optionally focus the content area once the picker is activated. 135 */ 136 start = async doFocus => { 137 if (this.isPicking) { 138 return; 139 } 140 this.isPicking = true; 141 this.doFocus = doFocus; 142 143 this.emit("picker-starting"); 144 145 this.targetCommand.watchTargets({ 146 types: this.targetCommand.ALL_TYPES, 147 onAvailable: this.#onTargetAvailable, 148 }); 149 150 this.emit("picker-started"); 151 }; 152 153 /** 154 * Stop the element picker. Note that the picker is automatically stopped when 155 * an element is picked. 156 * 157 * @param {boolean} isDestroyCodePath 158 * Optional. If true, we assume that's when the toolbox closes 159 * and we should avoid doing any RDP request. 160 * @param {boolean} canceled 161 * Optional. If true, emit an additional event to notify that the 162 * picker was canceled, ie stopped without selecting a node. 163 */ 164 stop = async ({ isDestroyCodepath, canceled } = {}) => { 165 if (!this.isPicking) { 166 return; 167 } 168 this.isPicking = false; 169 this.doFocus = false; 170 171 this.targetCommand.unwatchTargets({ 172 types: this.targetCommand.ALL_TYPES, 173 onAvailable: this.#onTargetAvailable, 174 }); 175 176 const promises = []; 177 for (const inspectorFront of this.#currentInspectorFronts) { 178 promises.push( 179 this.#onInspectorFrontDestroyed(inspectorFront, { 180 isDestroyCodepath, 181 }) 182 ); 183 } 184 await Promise.all(promises); 185 186 this.#currentInspectorFronts.clear(); 187 188 this.emit("picker-stopped"); 189 190 if (canceled) { 191 this.emit("picker-node-canceled"); 192 } 193 }; 194 195 destroy() { 196 // Do not await for stop as the isDestroy argument will make this method synchronous 197 // and we want to avoid having an async destroy 198 this.stop({ isDestroyCodepath: true }); 199 this.targetCommand = null; 200 this.commands = null; 201 } 202 203 /** 204 * When a node is hovered by the mouse when the highlighter is in picker mode 205 * 206 * @param {object} data 207 * Information about the node being hovered 208 */ 209 #onHovered = async data => { 210 // When debugging WebExtensions, Background page and popups are independent documents. 211 // None is the parent of each others. 212 // This means that if the toolbox is having the background page selected and you mouse over a popup, 213 // the popup DOM Element won't be in the markup view as that's not in a children document of the background page. 214 // Because of that, we have to select the hovered node's document and target in order to have it visible in the markup view. 215 // 216 // These top documents (background pages and popups) can actually have nested iframes, 217 // for these, we will also select these nested iframes, even if they could theoritically be shown from the top document. 218 if ( 219 this.targetCommand.descriptorFront.isWebExtensionDescriptor && 220 data.node.targetFront != this.targetCommand.selectedTargetFront 221 ) { 222 await this.targetCommand.selectTarget(data.node.targetFront); 223 } 224 225 this.emit("picker-node-hovered", data.node); 226 227 // We're going to cleanup references for all the other walkers, so that if we hover 228 // back the same node, we will receive a new `picker-node-hovered` event. 229 for (const inspectorFront of this.#currentInspectorFronts) { 230 if (inspectorFront.walker !== data.node.walkerFront) { 231 inspectorFront.walker.clearPicker(); 232 } 233 } 234 }; 235 236 /** 237 * When a node has been picked while the highlighter is in picker mode 238 * 239 * @param {object} data 240 * Information about the picked node 241 */ 242 #onPicked = data => { 243 this.emit("picker-node-picked", data.node); 244 return this.stop(); 245 }; 246 247 /** 248 * When a node has been shift-clicked (previewed) while the highlighter is in 249 * picker mode 250 * 251 * @param {object} data 252 * Information about the picked node 253 */ 254 #onPreviewed = data => { 255 this.emit("picker-node-previewed", data.node); 256 }; 257 258 /** 259 * When the picker is canceled, stop the picker, and make sure the toolbox 260 * gets the focus. 261 */ 262 #onCanceled = () => { 263 return this.stop({ canceled: true }); 264 }; 265 } 266 267 module.exports = NodePicker;