Bug 1259834 - Create basic HTML tooltip API;r=bgrins

First implementation of HTML based tooltip to be used in devtools
instead of XUL panels. API is similar to the current API of
Tooltip.js

MozReview-Commit-ID: 8njiKBubLSj

--HG--
extra : rebase_source : 930bf7aef48e6c16d7a560d261e2bfd06fe02a63
extra : source : 09874a1e6f2c942a1f9de827fedd14da7e67a6e5
This commit is contained in:
Julian Descottes 2016-05-04 14:44:57 +02:00
Родитель 7c22dba684
Коммит b873e263bd
13 изменённых файлов: 907 добавлений и 0 удалений

Просмотреть файл

@ -421,3 +421,21 @@ function waitForContextMenu(popup, button, onShown, onHidden) {
button.ownerDocument.defaultView);
return deferred.promise;
}
/**
* Simple helper to push a temporary preference. Wrapper on SpecialPowers
* pushPrefEnv that returns a promise resolving when the preferences have been
* updated.
*
* @param {String} preferenceName
* The name of the preference to updated
* @param {} value
* The preference value, type can vary
* @return {Promise} resolves when the preferences have been updated
*/
function pushPref(preferenceName, value) {
return new Promise(resolve => {
let options = {"set": [[preferenceName, value]]};
SpecialPowers.pushPrefEnv(options, resolve);
});
}

Просмотреть файл

@ -129,6 +129,7 @@ devtools.jar:
content/shared/widgets/spectrum-frame.xhtml (shared/widgets/spectrum-frame.xhtml)
content/shared/widgets/spectrum.css (shared/widgets/spectrum.css)
content/shared/widgets/cubic-bezier-frame.xhtml (shared/widgets/cubic-bezier-frame.xhtml)
content/shared/widgets/tooltip-frame.xhtml (shared/widgets/tooltip-frame.xhtml)
content/shared/widgets/cubic-bezier.css (shared/widgets/cubic-bezier.css)
content/shared/widgets/mdn-docs-frame.xhtml (shared/widgets/mdn-docs-frame.xhtml)
content/shared/widgets/mdn-docs.css (shared/widgets/mdn-docs.css)

Просмотреть файл

@ -10,6 +10,7 @@ support-files =
browser_devices.json
doc_options-view.xul
head.js
helper_html_tooltip.js
html-mdn-css-basic-testing.html
html-mdn-css-no-summary.html
html-mdn-css-no-summary-or-syntax.html
@ -111,6 +112,11 @@ skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
[browser_graphs-16.js]
skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
[browser_html_tooltip-01.js]
[browser_html_tooltip-02.js]
[browser_html_tooltip-03.js]
[browser_html_tooltip-04.js]
[browser_html_tooltip-05.js]
[browser_inplace-editor-01.js]
[browser_inplace-editor-02.js]
[browser_inplace-editor_maxwidth.js]

Просмотреть файл

@ -0,0 +1,71 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from helper_html_tooltip.js */
"use strict";
/**
* Test the HTMLTooltip show & hide methods.
*/
const HTML_NS = "http://www.w3.org/1999/xhtml";
const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://devtools/skin/common.css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="Tooltip test">
<vbox flex="1">
<hbox id="box1" flex="1">test1</hbox>
<hbox id="box2" flex="1">test2</hbox>
<hbox id="box3" flex="1">test3</hbox>
<hbox id="box4" flex="1">test4</hbox>
</vbox>
</window>`;
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
function getTooltipContent(doc) {
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "50px";
div.style.boxSizing = "border-box";
div.textContent = "tooltip";
return div;
}
add_task(function* () {
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
let tooltip = new HTMLTooltip({doc}, {});
info("Set tooltip content");
yield tooltip.setContent(getTooltipContent(doc), 100, 50);
is(tooltip.isVisible(), false, "Tooltip is not visible");
info("Show the tooltip and check the expected events are fired.");
let shown = 0;
tooltip.on("shown", () => shown++);
let onShown = tooltip.once("shown");
tooltip.show(doc.getElementById("box1"));
yield onShown;
is(shown, 1, "Event shown was fired once");
is(tooltip.isVisible(), true, "Tooltip is visible");
info("Hide the tooltip and check the expected events are fired.");
let hidden = 0;
tooltip.on("hidden", () => hidden++);
let onPopupHidden = tooltip.once("hidden");
tooltip.hide();
yield onPopupHidden;
is(hidden, 1, "Event hidden was fired once");
is(tooltip.isVisible(), false, "Tooltip is not visible");
});

Просмотреть файл

@ -0,0 +1,98 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from helper_html_tooltip.js */
"use strict";
/**
* Test the HTMLTooltip is closed when clicking outside of its container.
*/
const HTML_NS = "http://www.w3.org/1999/xhtml";
const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="Tooltip test">
<vbox flex="1">
<hbox id="box1" flex="1">test1</hbox>
<hbox id="box2" flex="1">test2</hbox>
<hbox id="box3" flex="1">test3</hbox>
<hbox id="box4" flex="1">test4</hbox>
</vbox>
</window>`;
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
add_task(function* () {
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
yield testTooltipNotClosingOnInsideClick(doc);
yield testConsumeOutsideClicksFalse(doc);
yield testConsumeOutsideClicksTrue(doc);
});
function* testTooltipNotClosingOnInsideClick(doc) {
info("Test a tooltip is not closed when clicking inside itself");
let tooltip = new HTMLTooltip({doc}, {});
yield tooltip.setContent(getTooltipContent(doc), 100, 50);
yield showTooltip(tooltip, doc.getElementById("box1"));
let onTooltipContainerClick = once(tooltip.container, "click");
EventUtils.synthesizeMouseAtCenter(tooltip.container, {}, doc.defaultView);
yield onTooltipContainerClick;
is(tooltip.isVisible(), true, "Tooltip is still visible");
tooltip.destroy();
}
function* testConsumeOutsideClicksFalse(doc) {
info("Test closing a tooltip via click with consumeOutsideClicks: false");
let box4 = doc.getElementById("box4");
let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: false});
yield tooltip.setContent(getTooltipContent(doc), 100, 50);
yield showTooltip(tooltip, doc.getElementById("box1"));
let onBox4Clicked = once(box4, "click");
let onHidden = once(tooltip, "hidden");
EventUtils.synthesizeMouseAtCenter(box4, {}, doc.defaultView);
yield onHidden;
yield onBox4Clicked;
is(tooltip.isVisible(), false, "Tooltip is hidden");
tooltip.destroy();
}
function* testConsumeOutsideClicksTrue(doc) {
info("Test closing a tooltip via click with consumeOutsideClicks: true");
let box4 = doc.getElementById("box4");
// Count clicks on box4
let box4clicks = 0;
box4.addEventListener("click", () => box4clicks++);
let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: true});
yield tooltip.setContent(getTooltipContent(doc), 100, 50);
yield showTooltip(tooltip, doc.getElementById("box1"));
let onHidden = once(tooltip, "hidden");
EventUtils.synthesizeMouseAtCenter(box4, {}, doc.defaultView);
yield onHidden;
is(box4clicks, 0, "box4 catched no click event");
is(tooltip.isVisible(), false, "Tooltip is hidden");
tooltip.destroy();
}
function getTooltipContent(doc) {
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "50px";
div.style.boxSizing = "border-box";
div.textContent = "tooltip";
return div;
}

Просмотреть файл

@ -0,0 +1,93 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from helper_html_tooltip.js */
"use strict";
/**
* Test the HTMLTooltip autofocus configuration option.
*/
const HTML_NS = "http://www.w3.org/1999/xhtml";
const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="Tooltip test">
<vbox flex="1">
<hbox id="box1" flex="1">test1</hbox>
<hbox id="box2" flex="1">test2</hbox>
<hbox id="box3" flex="1">test3</hbox>
<hbox id="box4" flex="1">
<textbox id="box4-input"></textbox>
</hbox>
</vbox>
</window>`;
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
add_task(function* () {
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
yield testTooltipWithAutoFocus(doc);
yield testTooltipWithoutAutoFocus(doc);
});
function* testTooltipWithAutoFocus(doc) {
info("Test a tooltip with autofocus takes focus when displayed");
let textbox = doc.querySelector("textbox");
info("Focus a XUL textbox");
let onInputFocus = once(textbox, "focus");
EventUtils.synthesizeMouseAtCenter(textbox, {}, doc.defaultView);
yield onInputFocus;
is(getFocusedDocument(doc), doc, "Focus is in the XUL document");
let tooltip = new HTMLTooltip({doc}, {autofocus: true});
let tooltipNode = getTooltipContent(doc);
yield tooltip.setContent(tooltipNode, 150, 50);
yield showTooltip(tooltip, doc.getElementById("box1"));
is(getFocusedDocument(doc), tooltipNode.ownerDocument,
"Focus is in the tooltip document");
yield hideTooltip(tooltip);
}
function* testTooltipWithoutAutoFocus(doc) {
info("Test a tooltip can be closed by clicking outside");
let textbox = doc.querySelector("textbox");
info("Focus a XUL textbox");
let onInputFocus = once(textbox, "focus");
EventUtils.synthesizeMouseAtCenter(textbox, {}, doc.defaultView);
yield onInputFocus;
is(getFocusedDocument(doc), doc, "Focus is in the XUL document");
let tooltip = new HTMLTooltip({doc}, {autofocus: false});
let tooltipNode = getTooltipContent(doc);
yield tooltip.setContent(tooltipNode, 150, 50);
yield showTooltip(tooltip, doc.getElementById("box1"));
is(getFocusedDocument(doc), doc, "Focus is still in the XUL document");
yield hideTooltip(tooltip);
}
function getFocusedDocument(doc) {
let activeElement = doc.activeElement;
while (activeElement && activeElement.contentDocument) {
activeElement = activeElement.contentDocument.activeElement;
}
return activeElement.ownerDocument;
}
function getTooltipContent(doc) {
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "50px";
div.style.boxSizing = "border-box";
return div;
}

Просмотреть файл

@ -0,0 +1,108 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from helper_html_tooltip.js */
"use strict";
/**
* Test the HTMLTooltip positioning for a small tooltip element (should aways
* find a way to fit).
*/
const HTML_NS = "http://www.w3.org/1999/xhtml";
const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://devtools/skin/common.css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="Tooltip test">
<vbox flex="1">
<hbox style="height: 10px">spacer</hbox>
<hbox id="box1" style="height: 50px">test1</hbox>
<hbox id="box2" style="height: 50px">test2</hbox>
<hbox flex="1">MIDDLE</hbox>
<hbox id="box3" style="height: 50px">test3</hbox>
<hbox id="box4" style="height: 50px">test4</hbox>
<hbox style="height: 10px">spacer</hbox>
</vbox>
</window>`;
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
const TOOLTIP_HEIGHT = 30;
const TOOLTIP_WIDTH = 100;
add_task(function* () {
// Force the toolbox to be 400px high;
yield pushPref("devtools.toolbox.footer.height", 400);
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
info("Create HTML tooltip");
let tooltip = new HTMLTooltip({doc}, {});
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "100%";
yield tooltip.setContent(div, TOOLTIP_WIDTH, TOOLTIP_HEIGHT);
let box1 = doc.getElementById("box1");
let box2 = doc.getElementById("box2");
let box3 = doc.getElementById("box3");
let box4 = doc.getElementById("box4");
let height = TOOLTIP_HEIGHT, width = TOOLTIP_WIDTH;
// box1: Can only fit below box1
info("Display the tooltip on box1.");
yield showTooltip(tooltip, box1);
let expectedTooltipGeometry = {position: "bottom", height, width};
checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
yield hideTooltip(tooltip);
info("Try to display the tooltip on top of box1.");
yield showTooltip(tooltip, box1, "top");
expectedTooltipGeometry = {position: "bottom", height, width};
checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
yield hideTooltip(tooltip);
// box2: Can fit above or below, will default to bottom, more height
// available.
info("Try to display the tooltip on box2.");
yield showTooltip(tooltip, box2);
expectedTooltipGeometry = {position: "bottom", height, width};
checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
yield hideTooltip(tooltip);
info("Try to display the tooltip on top of box2.");
yield showTooltip(tooltip, box2, "top");
expectedTooltipGeometry = {position: "top", height, width};
checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
yield hideTooltip(tooltip);
// box3: Can fit above or below, will default to top, more height available.
info("Try to display the tooltip on box3.");
yield showTooltip(tooltip, box3);
expectedTooltipGeometry = {position: "top", height, width};
checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
yield hideTooltip(tooltip);
info("Try to display the tooltip on bottom of box3.");
yield showTooltip(tooltip, box3, "bottom");
expectedTooltipGeometry = {position: "bottom", height, width};
checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
yield hideTooltip(tooltip);
// box4: Can only fit above box4
info("Display the tooltip on box4.");
yield showTooltip(tooltip, box4);
expectedTooltipGeometry = {position: "top", height, width};
checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
yield hideTooltip(tooltip);
info("Try to display the tooltip on bottom of box4.");
yield showTooltip(tooltip, box4, "bottom");
expectedTooltipGeometry = {position: "top", height, width};
checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
yield hideTooltip(tooltip);
is(tooltip.isVisible(), false, "Tooltip is not visible");
});

Просмотреть файл

@ -0,0 +1,108 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from helper_html_tooltip.js */
"use strict";
/**
* Test the HTMLTooltip positioning for a huge tooltip element (can not fit in
* the viewport).
*/
const HTML_NS = "http://www.w3.org/1999/xhtml";
const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://devtools/skin/common.css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="Tooltip test">
<vbox flex="1">
<hbox id="box1" style="height: 50px">test1</hbox>
<hbox id="box2" style="height: 50px">test2</hbox>
<hbox id="box3" style="height: 50px">test3</hbox>
<hbox id="box4" style="height: 50px">test4</hbox>
</vbox>
</window>`;
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
const TOOLTIP_HEIGHT = 200;
const TOOLTIP_WIDTH = 200;
add_task(function* () {
// Force the toolbox to be 200px high;
yield pushPref("devtools.toolbox.footer.height", 200);
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
info("Create HTML tooltip");
let tooltip = new HTMLTooltip({doc}, {});
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "100%";
yield tooltip.setContent(div, TOOLTIP_WIDTH, TOOLTIP_HEIGHT);
let box1 = doc.getElementById("box1");
let box2 = doc.getElementById("box2");
let box3 = doc.getElementById("box3");
let box4 = doc.getElementById("box4");
let width = TOOLTIP_WIDTH;
// box1: Can not fit above or below box1, default to bottom with a reduced
// height of 150px.
info("Display the tooltip on box1.");
yield showTooltip(tooltip, box1);
let expectedTooltipGeometry = {position: "bottom", height: 150, width};
checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
yield hideTooltip(tooltip);
info("Try to display the tooltip on top of box1.");
yield showTooltip(tooltip, box1, "top");
expectedTooltipGeometry = {position: "bottom", height: 150, width};
checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
yield hideTooltip(tooltip);
// box2: Can not fit above or below box2, default to bottom with a reduced
// height of 100px.
info("Try to display the tooltip on box2.");
yield showTooltip(tooltip, box2);
expectedTooltipGeometry = {position: "bottom", height: 100, width};
checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
yield hideTooltip(tooltip);
info("Try to display the tooltip on top of box2.");
yield showTooltip(tooltip, box2, "top");
expectedTooltipGeometry = {position: "bottom", height: 100, width};
checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
yield hideTooltip(tooltip);
// box3: Can not fit above or below box3, default to top with a reduced height
// of 100px.
info("Try to display the tooltip on box3.");
yield showTooltip(tooltip, box3);
expectedTooltipGeometry = {position: "top", height: 100, width};
checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
yield hideTooltip(tooltip);
info("Try to display the tooltip on bottom of box3.");
yield showTooltip(tooltip, box3, "bottom");
expectedTooltipGeometry = {position: "top", height: 100, width};
checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
yield hideTooltip(tooltip);
// box4: Can not fit above or below box4, default to top with a reduced height
// of 150px.
info("Display the tooltip on box4.");
yield showTooltip(tooltip, box4);
expectedTooltipGeometry = {position: "top", height: 150, width};
checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
yield hideTooltip(tooltip);
info("Try to display the tooltip on bottom of box4.");
yield showTooltip(tooltip, box4, "bottom");
expectedTooltipGeometry = {position: "top", height: 150, width};
checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
yield hideTooltip(tooltip);
is(tooltip.isVisible(), false, "Tooltip is not visible");
});

Просмотреть файл

@ -0,0 +1,78 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
"use strict";
/**
* Helper methods for the HTMLTooltip integration tests.
*/
/**
* Display an existing HTMLTooltip on an anchor. Returns a promise that will
* resolve when the tooltip "shown" event has been fired.
*
* @param {HTMLTooltip} tooltip
* The tooltip instance to display
* @param {Node} anchor
* The anchor that should be used to display the tooltip
* @param {String} position
* The preferred display position ("top", "bottom")
* @return {Promise} promise that resolves when the "shown" event is fired
*/
function showTooltip(tooltip, anchor, position) {
let onShown = tooltip.once("shown");
tooltip.show(anchor, {position});
return onShown;
}
/**
* Hide an existing HTMLTooltip. Returns a promise that will resolve when the
* tooltip "hidden" event has been fired.
*
* @param {HTMLTooltip} tooltip
* The tooltip instance to hide
* @return {Promise} promise that resolves when the "hidden" event is fired
*/
function hideTooltip(tooltip) {
let onPopupHidden = tooltip.once("hidden");
tooltip.hide();
return onPopupHidden;
}
/**
* Test helper designed to check that a tooltip is displayed at the expected
* position relative to an anchor, given a set of expectations.
*
* @param {HTMLTooltip} tooltip
* The HTMLTooltip instance to check
* @param {Node} anchor
* The tooltip's anchor
* @param {Object} expected
* - {String} position : "top" or "bottom"
* - {Boolean} leftAligned
* - {Number} width: expected tooltip width
* - {Number} height: expected tooltip height
*/
function checkTooltipGeometry(tooltip, anchor,
{position, leftAligned = true, height, width} = {}) {
info("Check the tooltip geometry matches expected position and dimensions");
let tooltipRect = tooltip.container.getBoundingClientRect();
let anchorRect = anchor.getBoundingClientRect();
if (position === "top") {
is(tooltipRect.bottom, anchorRect.top, "Tooltip is above the anchor");
} else if (position === "bottom") {
is(tooltipRect.top, anchorRect.bottom, "Tooltip is below the anchor");
} else {
ok(false, "Invalid position provided to checkTooltipGeometry");
}
if (leftAligned) {
is(tooltipRect.left, anchorRect.left,
"Tooltip left-aligned with the anchor");
}
is(tooltipRect.height, height, "Tooltip has the expected height");
is(tooltipRect.width, width, "Tooltip has the expected width");
}

Просмотреть файл

@ -0,0 +1,295 @@
/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const IFRAME_URL = "chrome://devtools/content/shared/widgets/tooltip-frame.xhtml";
const IFRAME_CONTAINER_ID = "tooltip-iframe-container";
/**
* The HTMLTooltip can display HTML content in a tooltip popup.
*
* @param {Toolbox} toolbox
* The devtools toolbox, needed to get the devtools main window.
* @param {Object}
* - {String} type
* Display type of the tooltip. Possible values: "normal"
* - {Boolean} autofocus
* Defaults to true. Should the tooltip be focused when opening it.
* - {Boolean} consumeOutsideClicks
* Defaults to true. The tooltip is closed when clicking outside.
* Should this event be stopped and consumed or not.
*/
function HTMLTooltip(toolbox,
{type = "normal", autofocus = true, consumeOutsideClicks = true} = {}) {
EventEmitter.decorate(this);
this.document = toolbox.doc;
this.type = type;
this.autofocus = autofocus;
this.consumeOutsideClicks = consumeOutsideClicks;
// Use the topmost window to listen for click events to close the tooltip
this.topWindow = this.document.defaultView.top;
this._onClick = this._onClick.bind(this);
this.container = this._createContainer();
// Promise that will resolve when the container can be filled with content.
this.containerReady = new Promise(resolve => {
if (this._isXUL()) {
// In XUL context, load a placeholder document in the iframe container.
let onLoad = () => {
this.container.removeEventListener("load", onLoad, true);
resolve();
};
this.container.addEventListener("load", onLoad, true);
this.container.setAttribute("src", IFRAME_URL);
} else {
// In non-XUL context the container is ready to use as is.
resolve();
}
});
}
module.exports.HTMLTooltip = HTMLTooltip;
HTMLTooltip.prototype = {
position: {
TOP: "top",
BOTTOM: "bottom",
},
get parent() {
if (this._isXUL()) {
// In XUL context, we are wrapping the HTML content in an iframe.
let win = this.container.contentWindow.wrappedJSObject;
return win.document.getElementById(IFRAME_CONTAINER_ID);
}
return this.container;
},
/**
* Set the tooltip content element. The preferred width/height should also be
* specified here.
*
* @param {Element} content
* The tooltip content, should be a HTML element.
* @param {Number} width
* Preferred width for the tooltip container
* @param {Number} height
* Preferred height for the tooltip container
* @return {Promise} a promise that will resolve when the content has been
* added in the tooltip container.
*/
setContent: function (content, width, height) {
this.preferredWidth = width;
this.preferredHeight = height;
return this.containerReady.then(() => {
this.parent.innerHTML = "";
this.parent.appendChild(content);
});
},
/**
* Show the tooltip next to the provided anchor element. A preferred position
* can be set. The event "shown" will be fired after the tooltip is displayed.
*
* @param {Element} anchor
* The reference element with which the tooltip should be aligned
* @param {Object}
* - {String} position: optional, possible values: top|bottom
* If layout permits, the tooltip will be displayed on top/bottom
* of the anchor. If ommitted, the tooltip will be displayed where
* more space is available.
*/
show: function (anchor, {position} = {}) {
this.containerReady.then(() => {
let {top, left, width, height} = this._findBestPosition(anchor, position);
if (this._isXUL()) {
this.container.setAttribute("width", width);
this.container.setAttribute("height", height);
} else {
this.container.style.width = width + "px";
this.container.style.height = height + "px";
}
this.container.style.top = top + "px";
this.container.style.left = left + "px";
this.container.style.display = "block";
if (this.autofocus) {
this.container.focus();
}
this.attachEventsTimer = this.document.defaultView.setTimeout(() => {
this.topWindow.addEventListener("click", this._onClick, true);
this.emit("shown");
}, 0);
});
},
/**
* Hide the current tooltip. The event "hidden" will be fired when the tooltip
* is hidden.
*/
hide: function () {
this.document.defaultView.clearTimeout(this.attachEventsTimer);
if (this.isVisible()) {
this.topWindow.removeEventListener("click", this._onClick, true);
this.container.style.display = "none";
this.emit("hidden");
}
},
/**
* Check if the tooltip is currently displayed.
* @return {Boolean} true if the tooltip is visible
*/
isVisible: function () {
let win = this.document.defaultView;
return win.getComputedStyle(this.container).display != "none";
},
/**
* Destroy the tooltip instance. Hide the tooltip if displayed, remove the
* tooltip container from the document.
*/
destroy: function () {
this.hide();
this.container.remove();
},
_createContainer: function () {
let container;
if (this._isXUL()) {
container = this.document.createElementNS(XHTML_NS, "iframe");
container.classList.add("devtools-tooltip-iframe");
this.document.querySelector("window").appendChild(container);
} else {
container = this.document.createElementNS(XHTML_NS, "div");
this.document.body.appendChild(container);
}
container.classList.add("theme-body");
container.classList.add("devtools-htmltooltip-container");
return container;
},
_onClick: function (e) {
if (this._isInTooltipContainer(e.target)) {
return;
}
this.hide();
if (this.consumeOutsideClicks) {
e.preventDefault();
e.stopPropagation();
}
},
_isInTooltipContainer: function (node) {
let contentWindow = this.parent.ownerDocument.defaultView;
let win = node.ownerDocument.defaultView;
if (win === contentWindow) {
// If node is in the same window as the tooltip, check if the tooltip
// parent contains node.
return this.parent.contains(node);
}
// Otherwise check if the node window is in the tooltip window.
while (win.parent && win.parent != win) {
win = win.parent;
if (win === contentWindow) {
return true;
}
}
return false;
},
_findBestPosition: function (anchor, position) {
let top, left;
let {TOP, BOTTOM} = this.position;
let {left: anchorLeft, top: anchorTop, height: anchorHeight}
= this._getRelativeRect(anchor, this.document);
let {bottom: docBottom, right: docRight} =
this.document.documentElement.getBoundingClientRect();
let height = this.preferredHeight;
// Check if the popup can fit above the anchor.
let availableTop = anchorTop;
let fitsAbove = availableTop >= height;
// Check if the popup can fit below the anchor.
let availableBelow = docBottom - (anchorTop + anchorHeight);
let fitsBelow = availableBelow >= height;
let isPositionSuitable = (fitsAbove && position === TOP)
|| (fitsBelow && position === BOTTOM);
if (!isPositionSuitable) {
// If the preferred position does not fit the preferred height,
// pick the position offering the most height.
position = availableTop > availableBelow ? TOP : BOTTOM;
}
// Calculate height, capped by the maximum height available.
height = Math.min(height, Math.max(availableTop, availableBelow));
top = position === TOP ? anchorTop - height : anchorTop + anchorHeight;
let availableWidth = docRight;
let width = Math.min(this.preferredWidth, availableWidth);
// By default, align the tooltip's left edge with the anchor left edge.
if (anchorLeft + width <= docRight) {
left = anchorLeft;
} else {
// If the tooltip cannot fit, shift to the left just enough to fit.
left = docRight - width;
}
return {top, left, width, height};
},
/**
* Get the bounding client rectangle for a given node, relative to a custom
* reference element (instead of the default for getBoundingClientRect which
* is always the element's ownerDocument).
*/
_getRelativeRect: function (node, relativeTo) {
// Width and Height can be taken from the rect.
let {width, height} = node.getBoundingClientRect();
// Find the smallest top/left coordinates from all quads.
let top = Infinity, left = Infinity;
let quads = node.getBoxQuads({relativeTo: relativeTo});
for (let quad of quads) {
top = Math.min(top, quad.bounds.top);
left = Math.min(left, quad.bounds.left);
}
// Compute right and bottom coordinates using the rest of the data.
let right = left + width;
let bottom = top + height;
return {top, right, bottom, left, width, height};
},
_isXUL: function () {
return this.document.documentElement.namespaceURI === XUL_NS;
},
};

Просмотреть файл

@ -20,6 +20,7 @@ DevToolsModules(
'FlameGraph.js',
'Graphs.js',
'GraphsWorker.js',
'HTMLTooltip.js',
'LineGraphWidget.js',
'MdnDocsWidget.js',
'MountainGraphWidget.js',

Просмотреть файл

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
<style>
html, body, #tooltip-iframe-container {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body role="application" class="theme-body">
<div id="tooltip-iframe-container"></div>
</body>
</html>

Просмотреть файл

@ -246,6 +246,12 @@
background: transparent;
}
.devtools-htmltooltip-container {
display: none;
position: fixed;
z-index: 9999;
}
/* links to source code, like displaying `myfile.js:45` */
.devtools-source-link {