summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/toolbox-highlighter-utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/toolbox-highlighter-utils.js')
-rw-r--r--devtools/client/framework/toolbox-highlighter-utils.js324
1 files changed, 324 insertions, 0 deletions
diff --git a/devtools/client/framework/toolbox-highlighter-utils.js b/devtools/client/framework/toolbox-highlighter-utils.js
new file mode 100644
index 000000000..e7f343857
--- /dev/null
+++ b/devtools/client/framework/toolbox-highlighter-utils.js
@@ -0,0 +1,324 @@
+/* -*- 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;
+};