/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* 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 promise = require("promise"); const {Task} = require("devtools/shared/task"); const flags = require("devtools/shared/flags"); /** * Client-side highlighter shared module. * To be used by toolbox panels that need to highlight DOM elements. * * Highlighting and selecting elements is common enough that it needs to be at * toolbox level, accessible by any panel that needs it. * That's why the toolbox is the one that initializes the inspector and * highlighter. It's also why the API returned by this module needs a reference * to the toolbox which should be set once only. */ /** * Get the highighterUtils instance for a given toolbox. * This should be done once only by the toolbox itself and stored there so that * panels can get it from there. That's because the API returned has a stateful * scope that would be different for another instance returned by this function. * * @param {Toolbox} toolbox * @return {Object} the highlighterUtils public API */ exports.getHighlighterUtils = function (toolbox) { if (!toolbox || !toolbox.target) { throw new Error("Missing or invalid toolbox passed to getHighlighterUtils"); return; } // Exported API properties will go here let exported = {}; // The current toolbox target let target = toolbox.target; // Is the highlighter currently in pick mode let isPicking = false; // Is the box model already displayed, used to prevent dispatching // unnecessary requests, especially during toolbox shutdown let isNodeFrontHighlighted = false; /** * Release this utils, nullifying the references to the toolbox */ exported.release = function () { toolbox = target = null; }; /** * Does the target have the highlighter actor. * The devtools must be backwards compatible with at least B2G 1.3 (28), * which doesn't have the highlighter actor. This can be removed as soon as * the minimal supported version becomes 1.4 (29) */ let isRemoteHighlightable = exported.isRemoteHighlightable = function () { return target.client.traits.highlightable; }; /** * Does the target support custom highlighters. */ let supportsCustomHighlighters = exported.supportsCustomHighlighters = () => { return !!target.client.traits.customHighlighters; }; /** * Make a function that initializes the inspector before it runs. * Since the init of the inspector is asynchronous, the return value will be * produced by Task.async and the argument should be a generator * @param {Function*} generator A generator function * @return {Function} A function */ let isInspectorInitialized = false; let requireInspector = generator => { return Task.async(function* (...args) { if (!isInspectorInitialized) { yield toolbox.initInspector(); isInspectorInitialized = true; } return yield generator.apply(null, args); }); }; /** * Start/stop the element picker on the debuggee target. * @param {Boolean} doFocus - Optionally focus the content area once the picker is * activated. * @return A promise that resolves when done */ let togglePicker = exported.togglePicker = function (doFocus) { if (isPicking) { return cancelPicker(); } else { return startPicker(doFocus); } }; /** * Start the element picker on the debuggee target. * This will request the inspector actor to start listening for mouse events * on the target page to highlight the hovered/picked element. * Depending on the server-side capabilities, this may fire events when nodes * are hovered. * @param {Boolean} doFocus - Optionally focus the content area once the picker is * activated. * @return A promise that resolves when the picker has started or immediately * if it is already started */ let startPicker = exported.startPicker = requireInspector(function* (doFocus = false) { if (isPicking) { return; } isPicking = true; toolbox.pickerButtonChecked = true; yield toolbox.selectTool("inspector"); toolbox.on("select", cancelPicker); if (isRemoteHighlightable()) { toolbox.walker.on("picker-node-hovered", onPickerNodeHovered); toolbox.walker.on("picker-node-picked", onPickerNodePicked); toolbox.walker.on("picker-node-previewed", onPickerNodePreviewed); toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled); yield toolbox.highlighter.pick(doFocus); toolbox.emit("picker-started"); } else { // If the target doesn't have the highlighter actor, we can use the // walker's pick method instead, knowing that it only responds when a node // is picked (instead of emitting events) toolbox.emit("picker-started"); let node = yield toolbox.walker.pick(); onPickerNodePicked({node: node}); } }); /** * Stop the element picker. Note that the picker is automatically stopped when * an element is picked * @return A promise that resolves when the picker has stopped or immediately * if it is already stopped */ let stopPicker = exported.stopPicker = requireInspector(function* () { if (!isPicking) { return; } isPicking = false; toolbox.pickerButtonChecked = false; if (isRemoteHighlightable()) { yield toolbox.highlighter.cancelPick(); toolbox.walker.off("picker-node-hovered", onPickerNodeHovered); toolbox.walker.off("picker-node-picked", onPickerNodePicked); toolbox.walker.off("picker-node-previewed", onPickerNodePreviewed); toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled); } else { // If the target doesn't have the highlighter actor, use the walker's // cancelPick method instead yield toolbox.walker.cancelPick(); } toolbox.off("select", cancelPicker); toolbox.emit("picker-stopped"); }); /** * Stop the picker, but also emit an event that the picker was canceled. */ let cancelPicker = exported.cancelPicker = Task.async(function* () { yield stopPicker(); toolbox.emit("picker-canceled"); }); /** * When a node is hovered by the mouse when the highlighter is in picker mode * @param {Object} data Information about the node being hovered */ function onPickerNodeHovered(data) { toolbox.emit("picker-node-hovered", data.node); } /** * When a node has been picked while the highlighter is in picker mode * @param {Object} data Information about the picked node */ function onPickerNodePicked(data) { toolbox.selection.setNodeFront(data.node, "picker-node-picked"); stopPicker(); } /** * When a node has been shift-clicked (previewed) while the highlighter is in * picker mode * @param {Object} data Information about the picked node */ function onPickerNodePreviewed(data) { toolbox.selection.setNodeFront(data.node, "picker-node-previewed"); } /** * When the picker is canceled, stop the picker, and make sure the toolbox * gets the focus. */ function onPickerNodeCanceled() { cancelPicker(); toolbox.win.focus(); } /** * Show the box model highlighter on a node in the content page. * The node needs to be a NodeFront, as defined by the inspector actor * @see devtools/server/actors/inspector.js * @param {NodeFront} nodeFront The node to highlight * @param {Object} options * @return A promise that resolves when the node has been highlighted */ let highlightNodeFront = exported.highlightNodeFront = requireInspector( function* (nodeFront, options = {}) { if (!nodeFront) { return; } isNodeFrontHighlighted = true; if (isRemoteHighlightable()) { yield toolbox.highlighter.showBoxModel(nodeFront, options); } else { // If the target doesn't have the highlighter actor, revert to the // walker's highlight method, which draws a simple outline yield toolbox.walker.highlight(nodeFront); } toolbox.emit("node-highlight", nodeFront, options.toSource()); }); /** * This is a convenience method in case you don't have a nodeFront but a * valueGrip. This is often the case with VariablesView properties. * This method will simply translate the grip into a nodeFront and call * highlightNodeFront, so it has the same signature. * @see highlightNodeFront */ let highlightDomValueGrip = exported.highlightDomValueGrip = requireInspector( function* (valueGrip, options = {}) { let nodeFront = yield gripToNodeFront(valueGrip); if (nodeFront) { yield highlightNodeFront(nodeFront, options); } else { throw new Error("The ValueGrip passed could not be translated to a NodeFront"); } }); /** * Translate a debugger value grip into a node front usable by the inspector * @param {ValueGrip} * @return a promise that resolves to the node front when done */ let gripToNodeFront = exported.gripToNodeFront = requireInspector( function* (grip) { return yield toolbox.walker.getNodeActorFromObjectActor(grip.actor); }); /** * Hide the highlighter. * @param {Boolean} forceHide Only really matters in test mode (when * flags.testing is true). In test mode, hovering over several nodes * in the markup view doesn't hide/show the highlighter to ease testing. The * highlighter stays visible at all times, except when the mouse leaves the * markup view, which is when this param is passed to true * @return a promise that resolves when the highlighter is hidden */ let unhighlight = exported.unhighlight = Task.async( function* (forceHide = false) { forceHide = forceHide || !flags.testing; // Note that if isRemoteHighlightable is true, there's no need to hide the // highlighter as the walker uses setTimeout to hide it after some time if (isNodeFrontHighlighted && forceHide && toolbox.highlighter && isRemoteHighlightable()) { isNodeFrontHighlighted = false; yield toolbox.highlighter.hideBoxModel(); } // unhighlight is called when destroying the toolbox, which means that by // now, the toolbox reference might have been nullified already. if (toolbox) { toolbox.emit("node-unhighlight"); } }); /** * If the main, box-model, highlighter isn't enough, or if multiple * highlighters are needed in parallel, this method can be used to return a * new instance of a highlighter actor, given a type. * The type of the highlighter passed must be known by the server. * The highlighter actor returned will have the show(nodeFront) and hide() * methods and needs to be released by the consumer when not needed anymore. * @return a promise that resolves to the highlighter */ let getHighlighterByType = exported.getHighlighterByType = requireInspector( function* (typeName) { let highlighter = null; if (supportsCustomHighlighters()) { highlighter = yield toolbox.inspector.getHighlighterByType(typeName); } return highlighter || promise.reject("The target doesn't support " + `creating highlighters by types or ${typeName} is unknown`); }); // Return the public API return exported; };