diff options
Diffstat (limited to 'devtools/server/actors/highlighters.js')
-rw-r--r-- | devtools/server/actors/highlighters.js | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/devtools/server/actors/highlighters.js b/devtools/server/actors/highlighters.js new file mode 100644 index 0000000000..a25bae4781 --- /dev/null +++ b/devtools/server/actors/highlighters.js @@ -0,0 +1,379 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Actor } = require("devtools/shared/protocol"); +const { customHighlighterSpec } = require("devtools/shared/specs/highlighters"); + +const EventEmitter = require("devtools/shared/event-emitter"); + +loader.lazyRequireGetter( + this, + "isXUL", + "resource://devtools/server/actors/highlighters/utils/markup.js", + true +); + +/** + * The registration mechanism for highlighters provides a quick way to + * have modular highlighters instead of a hard coded list. + */ +const highlighterTypes = new Map(); + +/** + * Returns `true` if a highlighter for the given `typeName` is registered, + * `false` otherwise. + */ +const isTypeRegistered = typeName => highlighterTypes.has(typeName); +exports.isTypeRegistered = isTypeRegistered; + +/** + * Registers a given constructor as highlighter, for the `typeName` given. + */ +const registerHighlighter = (typeName, modulePath) => { + if (highlighterTypes.has(typeName)) { + throw Error(`${typeName} is already registered.`); + } + + highlighterTypes.set(typeName, modulePath); +}; + +/** + * CustomHighlighterActor is a generic Actor that instantiates a custom implementation of + * a highlighter class given its type name which must be registered in `highlighterTypes`. + * CustomHighlighterActor proxies calls to methods of the highlighter class instance: + * constructor(targetActor), show(node, options), hide(), destroy() + */ +exports.CustomHighlighterActor = class CustomHighligherActor extends Actor { + /** + * Create a highlighter instance given its typeName. + */ + constructor(parent, typeName) { + super(parent.conn, customHighlighterSpec); + + this._parent = parent; + + const modulePath = highlighterTypes.get(typeName); + if (!modulePath) { + const list = [...highlighterTypes.keys()]; + + throw new Error(`${typeName} isn't a valid highlighter class (${list})`); + } + + const constructor = require(modulePath)[typeName]; + // The assumption is that custom highlighters either need the canvasframe + // container to append their elements and thus a non-XUL window or they have + // to define a static XULSupported flag that indicates that the highlighter + // supports XUL windows. Otherwise, bail out. + if (!isXUL(this._parent.targetActor.window) || constructor.XULSupported) { + this._highlighterEnv = new HighlighterEnvironment(); + this._highlighterEnv.initFromTargetActor(parent.targetActor); + this._highlighter = new constructor(this._highlighterEnv); + if (this._highlighter.on) { + this._highlighter.on( + "highlighter-event", + this._onHighlighterEvent.bind(this) + ); + } + } else { + throw new Error( + "Custom " + typeName + "highlighter cannot be created in a XUL window" + ); + } + } + + destroy() { + super.destroy(); + this.finalize(); + this._parent = null; + } + + release() {} + + /** + * Get current instance of the highlighter object. + */ + get instance() { + return this._highlighter; + } + + /** + * Show the highlighter. + * This calls through to the highlighter instance's |show(node, options)| + * method. + * + * Most custom highlighters are made to highlight DOM nodes, hence the first + * NodeActor argument (NodeActor as in devtools/server/actor/inspector). + * Note however that some highlighters use this argument merely as a context + * node: The SelectorHighlighter for instance uses it as a base node to run the + * provided CSS selector on. + * + * @param {NodeActor} The node to be highlighted + * @param {Object} Options for the custom highlighter + * @return {Boolean} True, if the highlighter has been successfully shown + */ + show(node, options) { + if (!this._highlighter) { + return null; + } + + const rawNode = node?.rawNode; + + return this._highlighter.show(rawNode, options); + } + + /** + * Hide the highlighter if it was shown before + */ + hide() { + if (this._highlighter) { + this._highlighter.hide(); + } + } + + /** + * Upon receiving an event from the highlighter, forward it to the client. + */ + _onHighlighterEvent(data) { + this.emit("highlighter-event", data); + } + + /** + * Destroy the custom highlighter implementation. + * This method is called automatically just before the actor is destroyed. + */ + finalize() { + if (this._highlighter) { + if (this._highlighter.off) { + this._highlighter.off( + "highlighter-event", + this._onHighlighterEvent.bind(this) + ); + } + this._highlighter.destroy(); + this._highlighter = null; + } + + if (this._highlighterEnv) { + this._highlighterEnv.destroy(); + this._highlighterEnv = null; + } + } +}; + +/** + * The HighlighterEnvironment is an object that holds all the required data for + * highlighters to work: the window, docShell, event listener target, ... + * It also emits "will-navigate", "navigate" and "window-ready" events, + * similarly to the WindowGlobalTargetActor. + * + * It can be initialized either from a WindowGlobalTargetActor (which is the + * most frequent way of using it, since highlighters are initialized by + * CustomHighlighterActor, which has a targetActor reference). + * It can also be initialized just with a window object (which is + * useful for when a highlighter is used outside of the devtools server context. + */ + +class HighlighterEnvironment extends EventEmitter { + initFromTargetActor(targetActor) { + this._targetActor = targetActor; + + const relayedEvents = [ + "window-ready", + "navigate", + "will-navigate", + "use-simple-highlighters-updated", + ]; + + this._abortController = new AbortController(); + const signal = this._abortController.signal; + for (const event of relayedEvents) { + this._targetActor.on(event, this.relayTargetEvent.bind(this, event), { + signal, + }); + } + } + + initFromWindow(win) { + this._win = win; + + // We need a progress listener to know when the window will navigate/has + // navigated. + const self = this; + this.listener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + onStateChange(progress, request, flag) { + const isStart = flag & Ci.nsIWebProgressListener.STATE_START; + const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; + const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW; + const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + + if (progress.DOMWindow !== win) { + return; + } + + if (isDocument && isStart) { + // One of the earliest events that tells us a new URI is being loaded + // in this window. + self.emit("will-navigate", { + window: win, + isTopLevel: true, + }); + } + if (isWindow && isStop) { + self.emit("navigate", { + window: win, + isTopLevel: true, + }); + } + }, + }; + + this.webProgress.addProgressListener( + this.listener, + Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT + ); + } + + get isInitialized() { + return this._win || this._targetActor; + } + + get isXUL() { + return isXUL(this.window); + } + + get useSimpleHighlightersForReducedMotion() { + return this._targetActor?._useSimpleHighlightersForReducedMotion; + } + + get window() { + if (!this.isInitialized) { + throw new Error( + "Initialize HighlighterEnvironment with a targetActor " + + "or window first" + ); + } + const win = this._targetActor ? this._targetActor.window : this._win; + + try { + return Cu.isDeadWrapper(win) ? null : win; + } catch (e) { + // win is null + return null; + } + } + + get document() { + return this.window && this.window.document; + } + + get docShell() { + return this.window && this.window.docShell; + } + + get webProgress() { + return ( + this.docShell && + this.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress) + ); + } + + /** + * Get the right target for listening to events on the page. + * - If the environment was initialized from a WindowGlobalTargetActor + * *and* if we're in the Browser Toolbox (to inspect Firefox Desktop): the + * targetActor is the RootActor, in which case, the window property can be + * used to listen to events. + * - With Firefox Desktop, the targetActor is a WindowGlobalTargetActor, and we use + * the chromeEventHandler which gives us a target we can use to listen to + * events, even from nested iframes. + * - If the environment was initialized from a window, we also use the + * chromeEventHandler. + */ + get pageListenerTarget() { + if (this._targetActor && this._targetActor.isRootActor) { + return this.window; + } + return this.docShell && this.docShell.chromeEventHandler; + } + + relayTargetEvent(name, data) { + this.emit(name, data); + } + + destroy() { + if (this._abortController) { + this._abortController.abort(); + this._abortController = null; + } + + // In case the environment was initialized from a window, we need to remove + // the progress listener. + if (this._win) { + try { + this.webProgress.removeProgressListener(this.listener); + } catch (e) { + // Which may fail in case the window was already destroyed. + } + } + + this._targetActor = null; + this._win = null; + } +} +exports.HighlighterEnvironment = HighlighterEnvironment; + +// This constant object is created to make the calls array more +// readable. Otherwise, linting rules force some array defs to span 4 +// lines instead, which is much harder to parse. +const HIGHLIGHTERS = { + accessible: "devtools/server/actors/highlighters/accessible", + boxModel: "devtools/server/actors/highlighters/box-model", + cssGrid: "devtools/server/actors/highlighters/css-grid", + cssTransform: "devtools/server/actors/highlighters/css-transform", + eyeDropper: "devtools/server/actors/highlighters/eye-dropper", + flexbox: "devtools/server/actors/highlighters/flexbox", + fonts: "devtools/server/actors/highlighters/fonts", + geometryEditor: "devtools/server/actors/highlighters/geometry-editor", + measuringTool: "devtools/server/actors/highlighters/measuring-tool", + pausedDebugger: "devtools/server/actors/highlighters/paused-debugger", + rulers: "devtools/server/actors/highlighters/rulers", + selector: "devtools/server/actors/highlighters/selector", + shapes: "devtools/server/actors/highlighters/shapes", + tabbingOrder: "devtools/server/actors/highlighters/tabbing-order", + viewportSize: "devtools/server/actors/highlighters/viewport-size", +}; + +// Each array in this array is called as register(arr[0], arr[1]). +const registerCalls = [ + ["AccessibleHighlighter", HIGHLIGHTERS.accessible], + ["BoxModelHighlighter", HIGHLIGHTERS.boxModel], + ["CssGridHighlighter", HIGHLIGHTERS.cssGrid], + ["CssTransformHighlighter", HIGHLIGHTERS.cssTransform], + ["EyeDropper", HIGHLIGHTERS.eyeDropper], + ["FlexboxHighlighter", HIGHLIGHTERS.flexbox], + ["FontsHighlighter", HIGHLIGHTERS.fonts], + ["GeometryEditorHighlighter", HIGHLIGHTERS.geometryEditor], + ["MeasuringToolHighlighter", HIGHLIGHTERS.measuringTool], + ["PausedDebuggerOverlay", HIGHLIGHTERS.pausedDebugger], + ["RulersHighlighter", HIGHLIGHTERS.rulers], + ["SelectorHighlighter", HIGHLIGHTERS.selector], + ["ShapesHighlighter", HIGHLIGHTERS.shapes], + ["TabbingOrderHighlighter", HIGHLIGHTERS.tabbingOrder], + ["ViewportSizeHighlighter", HIGHLIGHTERS.viewportSize], +]; + +// Register each highlighter above. +registerCalls.forEach(arr => { + registerHighlighter(arr[0], arr[1]); +}); |