highlighters.js (11423B)
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 const { Actor } = require("devtools/shared/protocol"); 8 const { customHighlighterSpec } = require("devtools/shared/specs/highlighters"); 9 const { TYPES } = ChromeUtils.importESModule( 10 "resource://devtools/shared/highlighters.mjs", 11 { global: "contextual" } 12 ); 13 14 const EventEmitter = require("devtools/shared/event-emitter"); 15 16 loader.lazyRequireGetter( 17 this, 18 "isXUL", 19 "resource://devtools/server/actors/highlighters/utils/markup.js", 20 true 21 ); 22 23 /** 24 * The registration mechanism for highlighters provides a quick way to 25 * have modular highlighters instead of a hard coded list. 26 */ 27 const highlighterTypes = new Map(); 28 29 /** 30 * Returns `true` if a highlighter for the given `typeName` is registered, 31 * `false` otherwise. 32 */ 33 const isTypeRegistered = typeName => highlighterTypes.has(typeName); 34 exports.isTypeRegistered = isTypeRegistered; 35 36 /** 37 * Registers a given constructor as highlighter, for the `typeName` given. 38 */ 39 const registerHighlighter = (typeName, modulePath) => { 40 if (highlighterTypes.has(typeName)) { 41 throw Error(`${typeName} is already registered.`); 42 } 43 44 highlighterTypes.set(typeName, modulePath); 45 }; 46 47 /** 48 * CustomHighlighterActor is a generic Actor that instantiates a custom implementation of 49 * a highlighter class given its type name which must be registered in `highlighterTypes`. 50 * CustomHighlighterActor proxies calls to methods of the highlighter class instance: 51 * constructor(nargetActor), show(node, options), hide(), destroy() 52 */ 53 exports.CustomHighlighterActor = class CustomHighligherActor extends Actor { 54 /** 55 * Create a highlighter instance given its typeName. 56 */ 57 constructor(parent, typeName) { 58 super(parent.conn, customHighlighterSpec); 59 60 this._parent = parent; 61 62 const modulePath = highlighterTypes.get(typeName); 63 if (!modulePath) { 64 const list = [...highlighterTypes.keys()]; 65 66 throw new Error(`${typeName} isn't a valid highlighter class (${list})`); 67 } 68 69 const constructor = require(modulePath)[typeName]; 70 // The assumption is that custom highlighters either need the canvasframe 71 // container to append their elements and thus a non-XUL window or they have 72 // to define a static XULSupported flag that indicates that the highlighter 73 // supports XUL windows. Otherwise, bail out. 74 if (!isXUL(this._parent.targetActor.window) || constructor.XULSupported) { 75 this._highlighterEnv = new HighlighterEnvironment(); 76 this._highlighterEnv.initFromTargetActor(parent.targetActor); 77 this._highlighter = new constructor(this._highlighterEnv, parent); 78 if (this._highlighter.on) { 79 this._highlighter.on( 80 "highlighter-event", 81 this._onHighlighterEvent.bind(this) 82 ); 83 } 84 } else { 85 throw new Error( 86 "Custom " + typeName + "highlighter cannot be created in a XUL window" 87 ); 88 } 89 } 90 91 destroy() { 92 super.destroy(); 93 this.finalize(); 94 this._parent = null; 95 } 96 97 release() {} 98 99 /** 100 * Get current instance of the highlighter object. 101 */ 102 get instance() { 103 return this._highlighter; 104 } 105 106 /** 107 * Show the highlighter. 108 * This calls through to the highlighter instance's |show(node, options)| 109 * method. 110 * 111 * Most custom highlighters are made to highlight DOM nodes, hence the first 112 * NodeActor argument (NodeActor as in devtools/server/actor/inspector). 113 * Note however that some highlighters use this argument merely as a context 114 * node: The SelectorHighlighter for instance uses it as a base node to run the 115 * provided CSS selector on. 116 * 117 * @param {NodeActor} The node to be highlighted 118 * @param {object} Options for the custom highlighter 119 * @return {boolean} True, if the highlighter has been successfully shown 120 */ 121 show(node, options) { 122 if (!this._highlighter) { 123 return null; 124 } 125 126 const rawNode = node?.rawNode; 127 128 return this._highlighter.show(rawNode, options); 129 } 130 131 /** 132 * Hide the highlighter if it was shown before 133 */ 134 hide() { 135 if (this._highlighter) { 136 this._highlighter.hide(); 137 } 138 } 139 140 /** 141 * Upon receiving an event from the highlighter, forward it to the client. 142 */ 143 _onHighlighterEvent(data) { 144 this.emit("highlighter-event", data); 145 } 146 147 /** 148 * Destroy the custom highlighter implementation. 149 * This method is called automatically just before the actor is destroyed. 150 */ 151 finalize() { 152 if (this._highlighter) { 153 if (this._highlighter.off) { 154 this._highlighter.off( 155 "highlighter-event", 156 this._onHighlighterEvent.bind(this) 157 ); 158 } 159 this._highlighter.destroy(); 160 this._highlighter = null; 161 } 162 163 if (this._highlighterEnv) { 164 this._highlighterEnv.destroy(); 165 this._highlighterEnv = null; 166 } 167 } 168 }; 169 170 /** 171 * The HighlighterEnvironment is an object that holds all the required data for 172 * highlighters to work: the window, docShell, event listener target, ... 173 * It also emits "will-navigate", "navigate" and "window-ready" events, 174 * similarly to the WindowGlobalTargetActor. 175 * 176 * It can be initialized either from a WindowGlobalTargetActor (which is the 177 * most frequent way of using it, since highlighters are initialized by 178 * CustomHighlighterActor, which has a targetActor reference). 179 * It can also be initialized just with a window object (which is 180 * useful for when a highlighter is used outside of the devtools server context. 181 */ 182 183 class HighlighterEnvironment extends EventEmitter { 184 initFromTargetActor(targetActor) { 185 this._targetActor = targetActor; 186 187 const relayedEvents = [ 188 "window-ready", 189 "navigate", 190 "will-navigate", 191 "use-simple-highlighters-updated", 192 ]; 193 194 this._abortController = new AbortController(); 195 const signal = this._abortController.signal; 196 for (const event of relayedEvents) { 197 this._targetActor.on(event, this.relayTargetEvent.bind(this, event), { 198 signal, 199 }); 200 } 201 } 202 203 initFromWindow(win) { 204 this._win = win; 205 206 // We need a progress listener to know when the window will navigate/has 207 // navigated. 208 const self = this; 209 this.listener = { 210 QueryInterface: ChromeUtils.generateQI([ 211 "nsIWebProgressListener", 212 "nsISupportsWeakReference", 213 ]), 214 215 onStateChange(progress, request, flag) { 216 const isStart = flag & Ci.nsIWebProgressListener.STATE_START; 217 const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; 218 const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW; 219 const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; 220 221 if (progress.DOMWindow !== win) { 222 return; 223 } 224 225 if (isDocument && isStart) { 226 // One of the earliest events that tells us a new URI is being loaded 227 // in this window. 228 self.emit("will-navigate", { 229 window: win, 230 isTopLevel: true, 231 }); 232 } 233 if (isWindow && isStop) { 234 self.emit("navigate", { 235 window: win, 236 isTopLevel: true, 237 }); 238 } 239 }, 240 }; 241 242 this.webProgress.addProgressListener( 243 this.listener, 244 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | 245 Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT 246 ); 247 } 248 249 get isInitialized() { 250 return this._win || this._targetActor; 251 } 252 253 get isXUL() { 254 return isXUL(this.window); 255 } 256 257 get useSimpleHighlightersForReducedMotion() { 258 return this._targetActor?._useSimpleHighlightersForReducedMotion; 259 } 260 261 get window() { 262 if (!this.isInitialized) { 263 throw new Error( 264 "Initialize HighlighterEnvironment with a targetActor " + 265 "or window first" 266 ); 267 } 268 const win = this._targetActor ? this._targetActor.window : this._win; 269 270 try { 271 return Cu.isDeadWrapper(win) ? null : win; 272 } catch (e) { 273 // win is null 274 return null; 275 } 276 } 277 278 get document() { 279 return this.window && this.window.document; 280 } 281 282 get docShell() { 283 return this.window && this.window.docShell; 284 } 285 286 get webProgress() { 287 return ( 288 this.docShell && 289 this.docShell 290 .QueryInterface(Ci.nsIInterfaceRequestor) 291 .getInterface(Ci.nsIWebProgress) 292 ); 293 } 294 295 /** 296 * Get the right target for listening to events on the page. 297 * - If the environment was initialized from a WindowGlobalTargetActor 298 * *and* if we're in the Browser Toolbox (to inspect Firefox Desktop): the 299 * targetActor is the RootActor, in which case, the window property can be 300 * used to listen to events. 301 * - With Firefox Desktop, the targetActor is a WindowGlobalTargetActor, and we use 302 * the chromeEventHandler which gives us a target we can use to listen to 303 * events, even from nested iframes. 304 * - If the environment was initialized from a window, we also use the 305 * chromeEventHandler. 306 */ 307 get pageListenerTarget() { 308 if (this._targetActor && this._targetActor.isRootActor) { 309 return this.window; 310 } 311 return ( 312 this._targetActor?.chromeEventHandler || this.docShell.chromeEventHandler 313 ); 314 } 315 316 relayTargetEvent(name, data) { 317 this.emit(name, data); 318 } 319 320 destroy() { 321 if (this._abortController) { 322 this._abortController.abort(); 323 this._abortController = null; 324 } 325 326 // In case the environment was initialized from a window, we need to remove 327 // the progress listener. 328 if (this._win) { 329 try { 330 this.webProgress.removeProgressListener(this.listener); 331 } catch (e) { 332 // Which may fail in case the window was already destroyed. 333 } 334 } 335 336 this._targetActor = null; 337 this._win = null; 338 } 339 } 340 exports.HighlighterEnvironment = HighlighterEnvironment; 341 342 // This constant object is created to make the calls array more 343 // readable. Otherwise, linting rules force some array defs to span 4 344 // lines instead, which is much harder to parse. 345 const HIGHLIGHTERS = { 346 [TYPES.ACCESSIBLE]: "devtools/server/actors/highlighters/accessible", 347 [TYPES.BOXMODEL]: "devtools/server/actors/highlighters/box-model", 348 [TYPES.GRID]: "devtools/server/actors/highlighters/css-grid", 349 [TYPES.TRANSFORM]: "devtools/server/actors/highlighters/css-transform", 350 [TYPES.EYEDROPPER]: "devtools/server/actors/highlighters/eye-dropper", 351 [TYPES.FLEXBOX]: "devtools/server/actors/highlighters/flexbox", 352 [TYPES.FONTS]: "devtools/server/actors/highlighters/fonts", 353 [TYPES.GEOMETRY]: "devtools/server/actors/highlighters/geometry-editor", 354 [TYPES.MEASURING]: "devtools/server/actors/highlighters/measuring-tool", 355 [TYPES.PAUSED_DEBUGGER]: 356 "devtools/server/actors/highlighters/paused-debugger", 357 [TYPES.RULERS]: "devtools/server/actors/highlighters/rulers", 358 [TYPES.SELECTOR]: "devtools/server/actors/highlighters/selector", 359 [TYPES.SHAPES]: "devtools/server/actors/highlighters/shapes", 360 [TYPES.TABBING_ORDER]: "devtools/server/actors/highlighters/tabbing-order", 361 [TYPES.VIEWPORT_SIZE]: "devtools/server/actors/highlighters/viewport-size", 362 [TYPES.VIEWPORT_SIZE_ON_RESIZE]: 363 "devtools/server/actors/highlighters/viewport-size-on-resize", 364 }; 365 for (const [typeName, modulePath] of Object.entries(HIGHLIGHTERS)) { 366 registerHighlighter(typeName, modulePath); 367 }