123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
- /* 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;
- };
|