summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/toolbox-highlighter-utils.js
blob: e7f34385766c89d8e9b02b2b63b411dba4ef1790 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
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;
};