Bug 1270462 - part1: extract devtools tooltip toggle logic to separate file;r=bgrins,jsnajdr
The code used to make the tooltip appear/disappear when hovering targets
has been extracted to a separate class that can be shared between the
current Tooltip.js implementation and the upcoming HTMLTooltip.
MozReview-Commit-ID: UYSjPFeMYK
--- a/devtools/client/debugger/debugger-view.js
+++ b/devtools/client/debugger/debugger-view.js
@@ -18,18 +18,16 @@ const GLOBAL_SEARCH_LINE_MAX_LENGTH = 30
const GLOBAL_SEARCH_ACTION_MAX_DELAY = 1500; // ms
const FUNCTION_SEARCH_ACTION_MAX_DELAY = 400; // ms
const SEARCH_GLOBAL_FLAG = "!";
const SEARCH_FUNCTION_FLAG = "@";
const SEARCH_TOKEN_FLAG = "#";
const SEARCH_LINE_FLAG = ":";
const SEARCH_VARIABLE_FLAG = "*";
const SEARCH_AUTOFILL = [SEARCH_GLOBAL_FLAG, SEARCH_FUNCTION_FLAG, SEARCH_TOKEN_FLAG];
-const EDITOR_VARIABLE_HOVER_DELAY = 750; // ms
-const EDITOR_VARIABLE_POPUP_POSITION = "topcenter bottomleft";
const TOOLBAR_ORDER_POPUP_POSITION = "topcenter bottomleft";
const RESIZE_REFRESH_RATE = 50; // ms
const PROMISE_DEBUGGER_URL =
"chrome://devtools/content/promisedebugger/promise-debugger.xhtml";
const EventListenersView = require('./content/views/event-listeners-view');
const SourcesView = require('./content/views/sources-view');
var actions = Object.assign(
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -841,17 +841,17 @@ function intendOpenVarPopup(aPanel, aPos
window.setTimeout(
function() {
if(tooltip.isEmpty()) {
deferred.resolve(false);
} else {
deferred.resolve(true);
}
},
- tooltip.defaultShowDelay + 1000
+ bubble.TOOLTIP_SHOW_DELAY + 1000
);
return deferred.promise;
}
function hideVarPopup(aPanel) {
let bubble = aPanel.panelWin.DebuggerView.VariableBubble;
let tooltip = bubble._tooltip.panel;
--- a/devtools/client/debugger/views/variable-bubble-view.js
+++ b/devtools/client/debugger/views/variable-bubble-view.js
@@ -21,16 +21,27 @@ function VariableBubbleView(DebuggerCont
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseOut = this._onMouseOut.bind(this);
this._onPopupHiding = this._onPopupHiding.bind(this);
}
VariableBubbleView.prototype = {
/**
+ * Delay before showing the variables bubble tooltip when hovering a valid
+ * target.
+ */
+ TOOLTIP_SHOW_DELAY: 750,
+
+ /**
+ * Tooltip position for the variables bubble tooltip.
+ */
+ TOOLTIP_POSITION: "topcenter bottomleft",
+
+ /**
* Initialization function, called when the debugger is started.
*/
initialize: function() {
dumpn("Initializing the VariableBubbleView");
this._toolbox = DebuggerController._toolbox;
this._editorContainer = document.getElementById("editor");
this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
@@ -44,18 +55,17 @@ VariableBubbleView.prototype = {
emitter: this._editorContainer,
event: "scroll",
useCapture: true
}, {
emitter: document,
event: "keydown"
}]
});
- this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION;
- this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY;
+ this._tooltip.defaultPosition = this.TOOLTIP_POSITION;
this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding);
},
/**
* Destruction function, called when the debugger is closed.
*/
destroy: function() {
dumpn("Destroying the VariableBubbleView");
@@ -270,17 +280,17 @@ VariableBubbleView.prototype = {
let isPopupVisible = !this._tooltip.isHidden();
if (isResumed || isSelecting || isPopupVisible) {
clearNamedTimeout("editor-mouse-move");
return;
}
// Allow events to settle down first. If the mouse hovers over
// a certain point in the editor long enough, try showing a variable bubble.
setNamedTimeout("editor-mouse-move",
- EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(e.clientX, e.clientY));
+ this.TOOLTIP_SHOW_DELAY, () => this._findIdentifier(e.clientX, e.clientY));
},
/**
* The mouseout listener for the source editor container node.
*/
_onMouseOut: function() {
clearNamedTimeout("editor-mouse-move");
},
--- a/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js
@@ -11,30 +11,30 @@ add_task(function* () {
let {inspector} = yield openInspectorForURL(TEST_URL);
let {markup} = inspector;
info("Get the tooltip target element for the image's src attribute");
let img = yield getContainerForSelector("img", inspector);
let target = img.editor.getAttributeElement("src").querySelector(".link");
info("Check that the src attribute of the image is a valid tooltip target");
- let isValid = yield markup.tooltip.isValidHoverTarget(target);
+ let isValid = yield isHoverTooltipTarget(markup.tooltip, target);
ok(isValid, "The element is a valid tooltip target");
info("Start dragging the test div");
yield simulateNodeDrag(inspector, "div");
info("Now check that the src attribute of the image isn't a valid target");
try {
- yield markup.tooltip.isValidHoverTarget(target);
+ yield isHoverTooltipTarget(markup.tooltip, target);
isValid = true;
} catch (e) {
isValid = false;
}
ok(!isValid, "The element is not a valid tooltip target");
info("Stop dragging the test div");
yield simulateNodeDrop(inspector, "div");
info("Check again the src attribute of the image");
- isValid = yield markup.tooltip.isValidHoverTarget(target);
+ isValid = yield isHoverTooltipTarget(markup.tooltip, target);
ok(isValid, "The element is a valid tooltip target");
});
--- a/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js
+++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js
@@ -38,17 +38,17 @@ function* getImageTooltipTarget({selecto
if (isImg) {
target = container.editor.getAttributeElement("src").querySelector(".link");
}
return target;
}
function* assertTooltipShownOn(element, {markup}) {
info("Is the element a valid hover target");
- let isValid = yield markup.tooltip.isValidHoverTarget(element);
+ let isValid = yield isHoverTooltipTarget(markup.tooltip, element);
ok(isValid, "The element is a valid hover target for the image tooltip");
}
function checkImageTooltip({selector, size}, {markup}) {
let images = markup.tooltip.panel.getElementsByTagName("image");
is(images.length, 1, "Tooltip for [" + selector + "] contains an image");
let label = markup.tooltip.panel.querySelector(".devtools-tooltip-caption");
--- a/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js
+++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js
@@ -32,28 +32,28 @@ add_task(function* () {
let container = getContainerForNodeFront(img, inspector);
ok(container, "Found markup container for the image.");
let target = container.editor.getAttributeElement("src")
.querySelector(".link");
ok(target, "Found the src attribute in the markup view.");
info("Showing tooltip on the src link.");
- yield inspector.markup.tooltip.isValidHoverTarget(target);
+ yield isHoverTooltipTarget(inspector.markup.tooltip, target);
checkImageTooltip(INITIAL_SRC_SIZE, inspector);
info("Updating the image src.");
yield updateImageSrc(img, UPDATED_SRC, inspector);
target = container.editor.getAttributeElement("src").querySelector(".link");
ok(target, "Found the src attribute in the markup view after mutation.");
info("Showing tooltip on the src link.");
- yield inspector.markup.tooltip.isValidHoverTarget(target);
+ yield isHoverTooltipTarget(inspector.markup.tooltip, target);
info("Checking that the new image was shown.");
checkImageTooltip(UPDATED_SRC_SIZE, inspector);
});
/**
* Updates the src attribute of the image. Return a Promise.
*/
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -165,46 +165,16 @@ var focusEditableField = Task.async(func
info("Editable field gained focus, returning the input field now");
let onEdit = inplaceEditor(editable.ownerDocument.activeElement);
return onEdit;
});
/**
- * Given a tooltip object instance (see Tooltip.js), checks if it is set to
- * toggle and hover and if so, checks if the given target is a valid hover
- * target. This won't actually show the tooltip (the less we interact with XUL
- * panels during test runs, the better).
- *
- * @return a promise that resolves when the answer is known
- */
-function isHoverTooltipTarget(tooltip, target) {
- if (!tooltip._basedNode || !tooltip.panel) {
- return promise.reject(new Error(
- "The tooltip passed isn't set to toggle on hover or is not a tooltip"));
- }
- return tooltip.isValidHoverTarget(target);
-}
-
-/**
- * Same as isHoverTooltipTarget except that it will fail the test if there is no
- * tooltip defined on hover of the given element
- *
- * @return a promise
- */
-function assertHoverTooltipOn(tooltip, element) {
- return isHoverTooltipTarget(tooltip, element).then(() => {
- ok(true, "A tooltip is defined on hover of the given element");
- }, () => {
- ok(false, "No tooltip is defined on hover of the given element");
- });
-}
-
-/**
* When a tooltip is closed, this ends up "commiting" the value changed within
* the tooltip (e.g. the color in case of a colorpicker) which, in turn, ends up
* setting the value of the corresponding css property in the rule-view.
* Use this function to close the tooltip and make sure the test waits for the
* ruleview-changed event.
* @param {Tooltip} tooltip
* @param {CSSRuleView} view
*/
--- a/devtools/client/inspector/shared/test/head.js
+++ b/devtools/client/inspector/shared/test/head.js
@@ -194,46 +194,16 @@ var focusEditableField = Task.async(func
info("Editable field gained focus, returning the input field now");
let onEdit = inplaceEditor(editable.ownerDocument.activeElement);
return onEdit;
});
/**
- * Given a tooltip object instance (see Tooltip.js), checks if it is set to
- * toggle and hover and if so, checks if the given target is a valid hover
- * target. This won't actually show the tooltip (the less we interact with XUL
- * panels during test runs, the better).
- *
- * @return a promise that resolves when the answer is known
- */
-function isHoverTooltipTarget(tooltip, target) {
- if (!tooltip._basedNode || !tooltip.panel) {
- return promise.reject(new Error(
- "The tooltip passed isn't set to toggle on hover or is not a tooltip"));
- }
- return tooltip.isValidHoverTarget(target);
-}
-
-/**
- * Same as isHoverTooltipTarget except that it will fail the test if there is no
- * tooltip defined on hover of the given element
- *
- * @return a promise
- */
-function assertHoverTooltipOn(tooltip, element) {
- return isHoverTooltipTarget(tooltip, element).then(() => {
- ok(true, "A tooltip is defined on hover of the given element");
- }, () => {
- ok(false, "No tooltip is defined on hover of the given element");
- });
-}
-
-/**
* Polls a given function waiting for it to return true.
*
* @param {Function} validatorFn
* A validator function that returns a boolean.
* This is called every few milliseconds to check if the result is true.
* When it is true, the promise resolves.
* @param {String} name
* Optional name of the test. This is used to generate
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -772,8 +772,38 @@ var waitForTab = Task.async(function*()
* @param {Window} win
* The window containing the panel
*/
function synthesizeKeys(input, win) {
for (let key of input.split("")) {
EventUtils.synthesizeKey(key, {}, win);
}
}
+
+/**
+ * Given a tooltip object instance (see Tooltip.js), checks if it is set to
+ * toggle and hover and if so, checks if the given target is a valid hover
+ * target. This won't actually show the tooltip (the less we interact with XUL
+ * panels during test runs, the better).
+ *
+ * @return a promise that resolves when the answer is known
+ */
+function isHoverTooltipTarget(tooltip, target) {
+ if (!tooltip._toggle._baseNode || !tooltip.panel) {
+ return promise.reject(new Error(
+ "The tooltip passed isn't set to toggle on hover or is not a tooltip"));
+ }
+ return tooltip._toggle.isValidHoverTarget(target);
+}
+
+/**
+ * Same as isHoverTooltipTarget except that it will fail the test if there is no
+ * tooltip defined on hover of the given element
+ *
+ * @return a promise
+ */
+function assertHoverTooltipOn(tooltip, element) {
+ return isHoverTooltipTarget(tooltip, element).then(() => {
+ ok(true, "A tooltip is defined on hover of the given element");
+ }, () => {
+ ok(false, "No tooltip is defined on hover of the given element");
+ });
+}
--- a/devtools/client/netmonitor/test/browser_net_image-tooltip.js
+++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
@@ -59,17 +59,18 @@ function test() {
});
}
/**
* @return a promise that resolves when the tooltip is shown
*/
function showTooltipOn(tooltip, element) {
return Task.spawn(function*() {
- let isTarget = yield tooltip.isValidHoverTarget(element);
+ let isValidTarget = yield tooltip._toggle.isValidHoverTarget(element);
+ ok(isValidTarget, "Element is a valid tooltip target");
let onShown = tooltip.once("shown");
tooltip.show();
yield onShown;
return tooltip;
});
}
aDebuggee.performRequests();
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -6,16 +6,17 @@
const {Cu, Ci} = require("chrome");
const promise = require("promise");
const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
const {CubicBezierWidget} =
require("devtools/client/shared/widgets/CubicBezierWidget");
const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
const EventEmitter = require("devtools/shared/event-emitter");
const {colorUtils} = require("devtools/client/shared/css-color");
const Heritage = require("sdk/core/heritage");
const {Eyedropper} = require("devtools/client/eyedropper/eyedropper");
const Editor = require("devtools/client/sourceeditor/editor");
const Services = require("Services");
loader.lazyRequireGetter(this, "beautify", "devtools/shared/jsbeautify/beautify");
@@ -183,18 +184,21 @@ function Tooltip(doc, options) {
this.options = new OptionsStore({
consumeOutsideClick: false,
closeOnKeys: [ESCAPE_KEYCODE],
noAutoFocus: true,
closeOnEvents: []
}, options);
this.panel = PanelFactory.get(doc, this.options);
- // Used for namedTimeouts in the mouseover handling
- this.uid = "tooltip-" + Date.now();
+ // Create tooltip toggle helper and decorate the Tooltip instance with
+ // shortcut methods.
+ this._toggle = new TooltipToggle(this);
+ this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
+ this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
// Emit show/hide events when the panel does.
for (let eventName of POPUP_EVENTS) {
this["_onPopup" + eventName] = (name => {
return e => {
if (e.target === this.panel) {
this.emit(name);
}
@@ -237,17 +241,16 @@ module.exports.Tooltip = Tooltip;
Tooltip.prototype = {
defaultPosition: "before_start",
// px
defaultOffsetX: 0,
// px
defaultOffsetY: 0,
// px
- defaultShowDelay: 50,
/**
* Show the tooltip. It might be wise to append some content first if you
* don't want the tooltip to be empty. You may access the content of the
* tooltip by setting a XUL node to t.content.
* @param {node} anchor
* Which node should the tooltip be shown on
* @param {string} position [optional]
@@ -328,155 +331,25 @@ Tooltip.prototype = {
emitter[remove](event, this.hide, useCapture);
break;
}
}
}
this.content = null;
- if (this._basedNode) {
- this.stopTogglingOnHover();
- }
+ this._toggle.destroy();
this.doc = null;
this.panel.remove();
this.panel = null;
},
/**
- * Show/hide the tooltip when the mouse hovers over particular nodes.
- *
- * 2 Ways to make this work:
- * - Provide a single node to attach the tooltip to, as the baseNode, and
- * omit the second targetNodeCb argument
- * - Provide a baseNode that is the container of possibly numerous children
- * elements that may receive a tooltip. In this case, provide the second
- * targetNodeCb argument to decide wether or not a child should receive
- * a tooltip.
- *
- * This works by tracking mouse movements on a base container node (baseNode)
- * and showing the tooltip when the mouse stops moving. The targetNodeCb
- * callback is used to know whether or not the particular element being
- * hovered over should indeed receive the tooltip. If you don't provide it
- * it's equivalent to a function that always returns true.
- *
- * Note that if you call this function a second time, it will itself call
- * stopTogglingOnHover before adding mouse tracking listeners again.
- *
- * @param {node} baseNode
- * The container for all target nodes
- * @param {Function} targetNodeCb
- * A function that accepts a node argument and returns true or false
- * (or a promise that resolves or rejects) to signify if the tooltip
- * should be shown on that node or not.
- * If the promise rejects, it must reject `false` as value.
- * Any other value is going to be logged as unexpected error.
- * Additionally, the function receives a second argument which is the
- * tooltip instance itself, to be used to add/modify the content of the
- * tooltip if needed. If omitted, the tooltip will be shown everytime.
- * @param {Number} showDelay
- * An optional delay that will be observed before showing the tooltip.
- * Defaults to this.defaultShowDelay.
- */
- startTogglingOnHover: function(baseNode, targetNodeCb,
- showDelay=this.defaultShowDelay) {
- if (this._basedNode) {
- this.stopTogglingOnHover();
- }
- if (!baseNode) {
- // Calling tool is in the process of being destroyed.
- return;
- }
-
- this._basedNode = baseNode;
- this._showDelay = showDelay;
- this._targetNodeCb = targetNodeCb || (() => true);
-
- this._onBaseNodeMouseMove = this._onBaseNodeMouseMove.bind(this);
- this._onBaseNodeMouseLeave = this._onBaseNodeMouseLeave.bind(this);
-
- baseNode.addEventListener("mousemove", this._onBaseNodeMouseMove, false);
- baseNode.addEventListener("mouseleave", this._onBaseNodeMouseLeave, false);
- },
-
- /**
- * If the startTogglingOnHover function has been used previously, and you want
- * to get rid of this behavior, then call this function to remove the mouse
- * movement tracking
- */
- stopTogglingOnHover: function() {
- clearNamedTimeout(this.uid);
-
- if (!this._basedNode) {
- return;
- }
-
- this._basedNode.removeEventListener("mousemove",
- this._onBaseNodeMouseMove, false);
- this._basedNode.removeEventListener("mouseleave",
- this._onBaseNodeMouseLeave, false);
-
- this._basedNode = null;
- this._targetNodeCb = null;
- this._lastHovered = null;
- },
-
- _onBaseNodeMouseMove: function(event) {
- if (event.target !== this._lastHovered) {
- this.hide();
- this._lastHovered = event.target;
- setNamedTimeout(this.uid, this._showDelay, () => {
- this.isValidHoverTarget(event.target).then(target => {
- this.show(target);
- }, reason => {
- if (reason === false) {
- // isValidHoverTarget rejects with false if the tooltip should
- // not be shown. This can be safely ignored.
- return;
- }
- // Report everything else. Reason might be error that should not be
- // hidden.
- console.error("isValidHoverTarget rejected with an unexpected reason:");
- console.error(reason);
- });
- });
- }
- },
-
- /**
- * Is the given target DOMNode a valid node for toggling the tooltip on hover.
- * This delegates to the user-defined _targetNodeCb callback.
- * @return a promise that resolves or rejects depending if the tooltip should
- * be shown or not. If it resolves, it does to the actual anchor to be used
- */
- isValidHoverTarget: function(target) {
- // Execute the user-defined callback which should return either true/false
- // or a promise that resolves or rejects
- let res = this._targetNodeCb(target, this);
-
- // The callback can additionally return a DOMNode to replace the anchor of
- // the tooltip when shown
- if (res && res.then) {
- return res.then(arg => {
- return arg instanceof Ci.nsIDOMNode ? arg : target;
- });
- }
- let newTarget = res instanceof Ci.nsIDOMNode ? res : target;
- return res ? promise.resolve(newTarget) : promise.reject(false);
- },
-
- _onBaseNodeMouseLeave: function() {
- clearNamedTimeout(this.uid);
- this._lastHovered = null;
- this.hide();
- },
-
- /**
* Set the content of this tooltip. Will first empty the tooltip and then
* append the new content element.
* Consider using one of the set<type>Content() functions instead.
* @param {node} content
* A node that can be appended in the tooltip XUL element
*/
set content(content) {
if (this.content == content) {
--- a/devtools/client/shared/widgets/moz.build
+++ b/devtools/client/shared/widgets/moz.build
@@ -1,14 +1,18 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
+DIRS += [
+ 'tooltip',
+]
+
DevToolsModules(
'AbstractTreeItem.jsm',
'BarGraphWidget.js',
'BreadcrumbsWidget.jsm',
'Chart.jsm',
'CubicBezierPresets.js',
'CubicBezierWidget.js',
'FastListWidget.js',
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/TooltipToggle.js
@@ -0,0 +1,152 @@
+/* -*- 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 DEFAULT_SHOW_DELAY = 50;
+
+/**
+ * Tooltip helper designed to show/hide the tooltip when the mouse hovers over
+ * particular nodes.
+ *
+ * This works by tracking mouse movements on a base container node (baseNode)
+ * and showing the tooltip when the mouse stops moving. A callback can be
+ * provided to the start() method to know whether or not the node being
+ * hovered over should indeed receive the tooltip.
+ */
+function TooltipToggle(tooltip) {
+ this.tooltip = tooltip;
+ this.win = tooltip.doc.defaultView;
+
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseLeave = this._onMouseLeave.bind(this);
+}
+
+module.exports.TooltipToggle = TooltipToggle;
+
+TooltipToggle.prototype = {
+ /**
+ * Start tracking mouse movements on the provided baseNode to show the
+ * tooltip.
+ *
+ * 2 Ways to make this work:
+ * - Provide a single node to attach the tooltip to, as the baseNode, and
+ * omit the second targetNodeCb argument
+ * - Provide a baseNode that is the container of possibly numerous children
+ * elements that may receive a tooltip. In this case, provide the second
+ * targetNodeCb argument to decide wether or not a child should receive
+ * a tooltip.
+ *
+ * Note that if you call this function a second time, it will itself call
+ * stop() before adding mouse tracking listeners again.
+ *
+ * @param {node} baseNode
+ * The container for all target nodes
+ * @param {Function} targetNodeCb
+ * A function that accepts a node argument and returns true or false
+ * (or a promise that resolves or rejects) to signify if the tooltip
+ * should be shown on that node or not.
+ * If the promise rejects, it must reject `false` as value.
+ * Any other value is going to be logged as unexpected error.
+ * Additionally, the function receives a second argument which is the
+ * tooltip instance itself, to be used to add/modify the content of the
+ * tooltip if needed. If omitted, the tooltip will be shown everytime.
+ * @param {Number} showDelay
+ * An optional delay that will be observed before showing the tooltip.
+ * Defaults to DEFAULT_SHOW_DELAY.
+ */
+ start: function (baseNode, targetNodeCb, showDelay = DEFAULT_SHOW_DELAY) {
+ this.stop();
+
+ if (!baseNode) {
+ // Calling tool is in the process of being destroyed.
+ return;
+ }
+
+ this._baseNode = baseNode;
+ this._showDelay = showDelay;
+ this._targetNodeCb = targetNodeCb || (() => true);
+
+ baseNode.addEventListener("mousemove", this._onMouseMove, false);
+ baseNode.addEventListener("mouseleave", this._onMouseLeave, false);
+ },
+
+ /**
+ * If the start() function has been used previously, and you want to get rid
+ * of this behavior, then call this function to remove the mouse movement
+ * tracking
+ */
+ stop: function () {
+ this.win.clearTimeout(this.toggleTimer);
+
+ if (!this._baseNode) {
+ return;
+ }
+
+ this._baseNode.removeEventListener("mousemove", this._onMouseMove, false);
+ this._baseNode.removeEventListener("mouseleave", this._onMouseLeave, false);
+
+ this._baseNode = null;
+ this._targetNodeCb = null;
+ this._lastHovered = null;
+ },
+
+ _onMouseMove: function (event) {
+ if (event.target !== this._lastHovered) {
+ this.tooltip.hide();
+ this._lastHovered = event.target;
+
+ this.win.clearTimeout(this.toggleTimer);
+ this.toggleTimer = this.win.setTimeout(() => {
+ this.isValidHoverTarget(event.target).then(target => {
+ this.tooltip.show(target);
+ }, reason => {
+ if (reason === false) {
+ // isValidHoverTarget rejects with false if the tooltip should
+ // not be shown. This can be safely ignored.
+ return;
+ }
+ console.error("isValidHoverTarget rejected with unexpected reason:");
+ console.error(reason);
+ });
+ }, this._showDelay);
+ }
+ },
+
+ /**
+ * Is the given target DOMNode a valid node for toggling the tooltip on hover.
+ * This delegates to the user-defined _targetNodeCb callback.
+ * @return a promise that resolves or rejects depending if the tooltip should
+ * be shown or not. If it resolves, it does to the actual anchor to be used
+ */
+ isValidHoverTarget: function (target) {
+ // Execute the user-defined callback which should return either true/false
+ // or a promise that resolves or rejects
+ let res = this._targetNodeCb(target, this.tooltip);
+
+ // The callback can additionally return a DOMNode to replace the anchor of
+ // the tooltip when shown
+ if (res && res.then) {
+ return res.then(arg => {
+ return arg && arg.nodeName ? arg : target;
+ });
+ }
+ let newTarget = res && res.nodeName ? res : target;
+ return new Promise((resolve, reject) => {
+ res ? resolve(newTarget) : reject(false);
+ });
+ },
+
+ _onMouseLeave: function () {
+ this.win.clearTimeout(this.toggleTimer);
+ this._lastHovered = null;
+ this.tooltip.hide();
+ },
+
+ destroy: function () {
+ this.stop();
+ }
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'TooltipToggle.js',
+)