toolbox-highlighter-utils.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const promise = require("promise");
  7. const {Task} = require("devtools/shared/task");
  8. const flags = require("devtools/shared/flags");
  9. /**
  10. * Client-side highlighter shared module.
  11. * To be used by toolbox panels that need to highlight DOM elements.
  12. *
  13. * Highlighting and selecting elements is common enough that it needs to be at
  14. * toolbox level, accessible by any panel that needs it.
  15. * That's why the toolbox is the one that initializes the inspector and
  16. * highlighter. It's also why the API returned by this module needs a reference
  17. * to the toolbox which should be set once only.
  18. */
  19. /**
  20. * Get the highighterUtils instance for a given toolbox.
  21. * This should be done once only by the toolbox itself and stored there so that
  22. * panels can get it from there. That's because the API returned has a stateful
  23. * scope that would be different for another instance returned by this function.
  24. *
  25. * @param {Toolbox} toolbox
  26. * @return {Object} the highlighterUtils public API
  27. */
  28. exports.getHighlighterUtils = function (toolbox) {
  29. if (!toolbox || !toolbox.target) {
  30. throw new Error("Missing or invalid toolbox passed to getHighlighterUtils");
  31. return;
  32. }
  33. // Exported API properties will go here
  34. let exported = {};
  35. // The current toolbox target
  36. let target = toolbox.target;
  37. // Is the highlighter currently in pick mode
  38. let isPicking = false;
  39. // Is the box model already displayed, used to prevent dispatching
  40. // unnecessary requests, especially during toolbox shutdown
  41. let isNodeFrontHighlighted = false;
  42. /**
  43. * Release this utils, nullifying the references to the toolbox
  44. */
  45. exported.release = function () {
  46. toolbox = target = null;
  47. };
  48. /**
  49. * Does the target have the highlighter actor.
  50. * The devtools must be backwards compatible with at least B2G 1.3 (28),
  51. * which doesn't have the highlighter actor. This can be removed as soon as
  52. * the minimal supported version becomes 1.4 (29)
  53. */
  54. let isRemoteHighlightable = exported.isRemoteHighlightable = function () {
  55. return target.client.traits.highlightable;
  56. };
  57. /**
  58. * Does the target support custom highlighters.
  59. */
  60. let supportsCustomHighlighters = exported.supportsCustomHighlighters = () => {
  61. return !!target.client.traits.customHighlighters;
  62. };
  63. /**
  64. * Make a function that initializes the inspector before it runs.
  65. * Since the init of the inspector is asynchronous, the return value will be
  66. * produced by Task.async and the argument should be a generator
  67. * @param {Function*} generator A generator function
  68. * @return {Function} A function
  69. */
  70. let isInspectorInitialized = false;
  71. let requireInspector = generator => {
  72. return Task.async(function* (...args) {
  73. if (!isInspectorInitialized) {
  74. yield toolbox.initInspector();
  75. isInspectorInitialized = true;
  76. }
  77. return yield generator.apply(null, args);
  78. });
  79. };
  80. /**
  81. * Start/stop the element picker on the debuggee target.
  82. * @param {Boolean} doFocus - Optionally focus the content area once the picker is
  83. * activated.
  84. * @return A promise that resolves when done
  85. */
  86. let togglePicker = exported.togglePicker = function (doFocus) {
  87. if (isPicking) {
  88. return cancelPicker();
  89. } else {
  90. return startPicker(doFocus);
  91. }
  92. };
  93. /**
  94. * Start the element picker on the debuggee target.
  95. * This will request the inspector actor to start listening for mouse events
  96. * on the target page to highlight the hovered/picked element.
  97. * Depending on the server-side capabilities, this may fire events when nodes
  98. * are hovered.
  99. * @param {Boolean} doFocus - Optionally focus the content area once the picker is
  100. * activated.
  101. * @return A promise that resolves when the picker has started or immediately
  102. * if it is already started
  103. */
  104. let startPicker = exported.startPicker = requireInspector(function* (doFocus = false) {
  105. if (isPicking) {
  106. return;
  107. }
  108. isPicking = true;
  109. toolbox.pickerButtonChecked = true;
  110. yield toolbox.selectTool("inspector");
  111. toolbox.on("select", cancelPicker);
  112. if (isRemoteHighlightable()) {
  113. toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
  114. toolbox.walker.on("picker-node-picked", onPickerNodePicked);
  115. toolbox.walker.on("picker-node-previewed", onPickerNodePreviewed);
  116. toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled);
  117. yield toolbox.highlighter.pick(doFocus);
  118. toolbox.emit("picker-started");
  119. } else {
  120. // If the target doesn't have the highlighter actor, we can use the
  121. // walker's pick method instead, knowing that it only responds when a node
  122. // is picked (instead of emitting events)
  123. toolbox.emit("picker-started");
  124. let node = yield toolbox.walker.pick();
  125. onPickerNodePicked({node: node});
  126. }
  127. });
  128. /**
  129. * Stop the element picker. Note that the picker is automatically stopped when
  130. * an element is picked
  131. * @return A promise that resolves when the picker has stopped or immediately
  132. * if it is already stopped
  133. */
  134. let stopPicker = exported.stopPicker = requireInspector(function* () {
  135. if (!isPicking) {
  136. return;
  137. }
  138. isPicking = false;
  139. toolbox.pickerButtonChecked = false;
  140. if (isRemoteHighlightable()) {
  141. yield toolbox.highlighter.cancelPick();
  142. toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
  143. toolbox.walker.off("picker-node-picked", onPickerNodePicked);
  144. toolbox.walker.off("picker-node-previewed", onPickerNodePreviewed);
  145. toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled);
  146. } else {
  147. // If the target doesn't have the highlighter actor, use the walker's
  148. // cancelPick method instead
  149. yield toolbox.walker.cancelPick();
  150. }
  151. toolbox.off("select", cancelPicker);
  152. toolbox.emit("picker-stopped");
  153. });
  154. /**
  155. * Stop the picker, but also emit an event that the picker was canceled.
  156. */
  157. let cancelPicker = exported.cancelPicker = Task.async(function* () {
  158. yield stopPicker();
  159. toolbox.emit("picker-canceled");
  160. });
  161. /**
  162. * When a node is hovered by the mouse when the highlighter is in picker mode
  163. * @param {Object} data Information about the node being hovered
  164. */
  165. function onPickerNodeHovered(data) {
  166. toolbox.emit("picker-node-hovered", data.node);
  167. }
  168. /**
  169. * When a node has been picked while the highlighter is in picker mode
  170. * @param {Object} data Information about the picked node
  171. */
  172. function onPickerNodePicked(data) {
  173. toolbox.selection.setNodeFront(data.node, "picker-node-picked");
  174. stopPicker();
  175. }
  176. /**
  177. * When a node has been shift-clicked (previewed) while the highlighter is in
  178. * picker mode
  179. * @param {Object} data Information about the picked node
  180. */
  181. function onPickerNodePreviewed(data) {
  182. toolbox.selection.setNodeFront(data.node, "picker-node-previewed");
  183. }
  184. /**
  185. * When the picker is canceled, stop the picker, and make sure the toolbox
  186. * gets the focus.
  187. */
  188. function onPickerNodeCanceled() {
  189. cancelPicker();
  190. toolbox.win.focus();
  191. }
  192. /**
  193. * Show the box model highlighter on a node in the content page.
  194. * The node needs to be a NodeFront, as defined by the inspector actor
  195. * @see devtools/server/actors/inspector.js
  196. * @param {NodeFront} nodeFront The node to highlight
  197. * @param {Object} options
  198. * @return A promise that resolves when the node has been highlighted
  199. */
  200. let highlightNodeFront = exported.highlightNodeFront = requireInspector(
  201. function* (nodeFront, options = {}) {
  202. if (!nodeFront) {
  203. return;
  204. }
  205. isNodeFrontHighlighted = true;
  206. if (isRemoteHighlightable()) {
  207. yield toolbox.highlighter.showBoxModel(nodeFront, options);
  208. } else {
  209. // If the target doesn't have the highlighter actor, revert to the
  210. // walker's highlight method, which draws a simple outline
  211. yield toolbox.walker.highlight(nodeFront);
  212. }
  213. toolbox.emit("node-highlight", nodeFront, options.toSource());
  214. });
  215. /**
  216. * This is a convenience method in case you don't have a nodeFront but a
  217. * valueGrip. This is often the case with VariablesView properties.
  218. * This method will simply translate the grip into a nodeFront and call
  219. * highlightNodeFront, so it has the same signature.
  220. * @see highlightNodeFront
  221. */
  222. let highlightDomValueGrip = exported.highlightDomValueGrip = requireInspector(
  223. function* (valueGrip, options = {}) {
  224. let nodeFront = yield gripToNodeFront(valueGrip);
  225. if (nodeFront) {
  226. yield highlightNodeFront(nodeFront, options);
  227. } else {
  228. throw new Error("The ValueGrip passed could not be translated to a NodeFront");
  229. }
  230. });
  231. /**
  232. * Translate a debugger value grip into a node front usable by the inspector
  233. * @param {ValueGrip}
  234. * @return a promise that resolves to the node front when done
  235. */
  236. let gripToNodeFront = exported.gripToNodeFront = requireInspector(
  237. function* (grip) {
  238. return yield toolbox.walker.getNodeActorFromObjectActor(grip.actor);
  239. });
  240. /**
  241. * Hide the highlighter.
  242. * @param {Boolean} forceHide Only really matters in test mode (when
  243. * flags.testing is true). In test mode, hovering over several nodes
  244. * in the markup view doesn't hide/show the highlighter to ease testing. The
  245. * highlighter stays visible at all times, except when the mouse leaves the
  246. * markup view, which is when this param is passed to true
  247. * @return a promise that resolves when the highlighter is hidden
  248. */
  249. let unhighlight = exported.unhighlight = Task.async(
  250. function* (forceHide = false) {
  251. forceHide = forceHide || !flags.testing;
  252. // Note that if isRemoteHighlightable is true, there's no need to hide the
  253. // highlighter as the walker uses setTimeout to hide it after some time
  254. if (isNodeFrontHighlighted && forceHide && toolbox.highlighter && isRemoteHighlightable()) {
  255. isNodeFrontHighlighted = false;
  256. yield toolbox.highlighter.hideBoxModel();
  257. }
  258. // unhighlight is called when destroying the toolbox, which means that by
  259. // now, the toolbox reference might have been nullified already.
  260. if (toolbox) {
  261. toolbox.emit("node-unhighlight");
  262. }
  263. });
  264. /**
  265. * If the main, box-model, highlighter isn't enough, or if multiple
  266. * highlighters are needed in parallel, this method can be used to return a
  267. * new instance of a highlighter actor, given a type.
  268. * The type of the highlighter passed must be known by the server.
  269. * The highlighter actor returned will have the show(nodeFront) and hide()
  270. * methods and needs to be released by the consumer when not needed anymore.
  271. * @return a promise that resolves to the highlighter
  272. */
  273. let getHighlighterByType = exported.getHighlighterByType = requireInspector(
  274. function* (typeName) {
  275. let highlighter = null;
  276. if (supportsCustomHighlighters()) {
  277. highlighter = yield toolbox.inspector.getHighlighterByType(typeName);
  278. }
  279. return highlighter || promise.reject("The target doesn't support " +
  280. `creating highlighters by types or ${typeName} is unknown`);
  281. });
  282. // Return the public API
  283. return exported;
  284. };