Bug 1267403 - HTMLTooltip: add useXulWrapper option when displayed in a XUL document;r=ochameau

The HTMLTooltip supports an additional configuration parameter "useXulWrapper".
When set to true, if the tooltip is displayed in a XUL document, a XUL panel
will be used as an additional container for the tooltip.

This allows the tooltip to be displayed anywhere on the screen and can be
useful when displayed in small toolboxes.

MozReview-Commit-ID: 63kv4vAeW5R

--HG--
extra : source : fc4d902ff01ee92a5b6742d44286e5feaaba1500
extra : intermediate-source : 126f43ff3be5505920946a77ad82401c6bbaebef
extra : histedit_source : 863888c014723f7e95742079395497ba1a30aa36%2C13ba9aaf80acb96c587739c767c20a8f0f6a9a5a
This commit is contained in:
Julian Descottes 2016-07-06 14:50:44 +02:00
Родитель a8cae36571
Коммит 3aa417586a
15 изменённых файлов: 388 добавлений и 83 удалений

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

@ -124,6 +124,7 @@ skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
[browser_html_tooltip_offset.js]
[browser_html_tooltip_variable-height.js]
[browser_html_tooltip_width-auto.js]
[browser_html_tooltip_xul-wrapper.js]
[browser_inplace-editor-01.js]
[browser_inplace-editor-02.js]
[browser_inplace-editor_autocomplete_01.js]

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

@ -9,7 +9,7 @@
*/
const HTML_NS = "http://www.w3.org/1999/xhtml";
const TEST_WINDOW_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
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/tooltips.css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
@ -22,19 +22,11 @@ const TEST_WINDOW_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
</vbox>
</window>`;
const TEST_PAGE_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/tooltips.css"?>
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="Tooltip test with document using a Page element">
<vbox flex="1">
<hbox id="box1" flex="1">test1</hbox>
</vbox>
</page>`;
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
let useXulWrapper;
function getTooltipContent(doc) {
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "50px";
@ -44,18 +36,20 @@ function getTooltipContent(doc) {
}
add_task(function* () {
info("Test showing a basic tooltip in XUL document using <window>");
yield testTooltipForUri(TEST_WINDOW_URI);
let [,, doc] = yield createHost("bottom", TEST_URI);
info("Test showing a basic tooltip in XUL document using <page>");
yield testTooltipForUri(TEST_PAGE_URI);
info("Run tests for a Tooltip without using a XUL panel");
useXulWrapper = false;
yield runTests(doc);
info("Run tests for a Tooltip with a XUL panel");
useXulWrapper = true;
yield runTests(doc);
});
function* testTooltipForUri(uri) {
let tab = yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", uri);
let tooltip = new HTMLTooltip({doc}, {});
function* runTests(doc) {
yield addTab("about:blank");
let tooltip = new HTMLTooltip({doc}, {useXulWrapper});
info("Set tooltip content");
tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
@ -90,5 +84,5 @@ function* testTooltipForUri(uri) {
yield waitForReflow(tooltip);
is(tooltip.isVisible(), false, "Tooltip is not visible");
yield removeTab(tab);
tooltip.destroy();
}

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

@ -27,21 +27,33 @@ const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
let useXulWrapper;
add_task(function* () {
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
info("Run tests for a Tooltip without using a XUL panel");
useXulWrapper = false;
yield runTests(doc);
info("Run tests for a Tooltip with a XUL panel");
useXulWrapper = true;
yield runTests(doc);
});
function* runTests(doc) {
yield testClickInTooltipContent(doc);
yield testConsumeOutsideClicksFalse(doc);
yield testConsumeOutsideClicksTrue(doc);
yield testClickInOuterIframe(doc);
yield testClickInInnerIframe(doc);
});
}
function* testClickInTooltipContent(doc) {
info("Test a tooltip is not closed when clicking inside itself");
let tooltip = new HTMLTooltip({doc}, {});
let tooltip = new HTMLTooltip({doc}, {useXulWrapper});
tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
yield showTooltip(tooltip, doc.getElementById("box1"));
@ -57,7 +69,7 @@ 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});
let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: false, useXulWrapper});
tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
yield showTooltip(tooltip, doc.getElementById("box1"));
@ -80,7 +92,7 @@ function* testConsumeOutsideClicksTrue(doc) {
let box4clicks = 0;
box4.addEventListener("click", () => box4clicks++);
let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: true});
let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: true, useXulWrapper});
tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
yield showTooltip(tooltip, doc.getElementById("box1"));
@ -98,7 +110,7 @@ function* testClickInOuterIframe(doc) {
info("Test clicking an iframe outside of the tooltip closes the tooltip");
let frame = doc.getElementById("frame");
let tooltip = new HTMLTooltip({doc});
let tooltip = new HTMLTooltip({doc}, {useXulWrapper});
tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
yield showTooltip(tooltip, doc.getElementById("box1"));
@ -113,7 +125,7 @@ function* testClickInOuterIframe(doc) {
function* testClickInInnerIframe(doc) {
info("Test clicking an iframe inside the tooltip content does not close the tooltip");
let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: false});
let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: false, useXulWrapper});
let iframe = doc.createElementNS(HTML_NS, "iframe");
iframe.style.width = "100px";

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

@ -31,14 +31,26 @@ const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
let useXulWrapper;
add_task(function* () {
yield addTab("about:blank");
let [, , doc] = yield createHost("bottom", TEST_URI);
info("Run tests for a Tooltip without using a XUL panel");
useXulWrapper = false;
yield runTests(doc);
info("Run tests for a Tooltip with a XUL panel");
useXulWrapper = true;
yield runTests(doc);
});
function* runTests(doc) {
yield testNoAutoFocus(doc);
yield testAutoFocus(doc);
yield testAutoFocusPreservesFocusChange(doc);
});
}
function* testNoAutoFocus(doc) {
yield focusNode(doc, "#box4-input");
@ -126,7 +138,7 @@ function blurNode(doc, selector) {
* tooltip content will be ready.
*/
function* createTooltip(doc, autofocus) {
let tooltip = new HTMLTooltip({doc}, {autofocus});
let tooltip = new HTMLTooltip({doc}, {autofocus, useXulWrapper});
let div = doc.createElementNS(HTML_NS, "div");
div.classList.add("tooltip-content");
div.style.height = "50px";

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

@ -40,7 +40,7 @@ add_task(function* () {
let [,, doc] = yield createHost("bottom", TEST_URI);
info("Create HTML tooltip");
let tooltip = new HTMLTooltip({doc}, {});
let tooltip = new HTMLTooltip({doc}, {useXulWrapper: false});
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "100%";
tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});

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

@ -32,12 +32,11 @@ 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 tooltip = new HTMLTooltip({doc}, {useXulWrapper: false});
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "100%";
tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
@ -57,7 +56,7 @@ add_task(function* () {
yield hideTooltip(tooltip);
info("Try to display the tooltip on top of box1.");
yield showTooltip(tooltip, box1, "top");
yield showTooltip(tooltip, box1, {position: "top"});
expectedTooltipGeometry = {position: "bottom", height: 150, width};
checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
yield hideTooltip(tooltip);

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

@ -49,6 +49,8 @@ const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
let useXulWrapper;
add_task(function* () {
// Force the toolbox to be 200px high;
yield pushPref("devtools.toolbox.footer.height", 200);
@ -56,8 +58,18 @@ add_task(function* () {
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
info("Run tests for a Tooltip without using a XUL panel");
useXulWrapper = false;
yield runTests(doc);
info("Run tests for a Tooltip with a XUL panel");
useXulWrapper = true;
yield runTests(doc);
});
function* runTests(doc) {
info("Create HTML tooltip");
let tooltip = new HTMLTooltip({doc}, {type: "arrow"});
let tooltip = new HTMLTooltip({doc}, {type: "arrow", useXulWrapper});
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "35px";
tooltip.setContent(div, {width: 200, height: 35});
@ -91,4 +103,6 @@ add_task(function* () {
yield hideTooltip(tooltip);
}
});
tooltip.destroy();
}

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

@ -43,15 +43,26 @@ const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
let useXulWrapper;
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("Run tests for a Tooltip without using a XUL panel");
useXulWrapper = false;
yield runTests(doc);
info("Run tests for a Tooltip with a XUL panel");
useXulWrapper = true;
yield runTests(doc);
});
function* runTests(doc) {
info("Create HTML tooltip");
let tooltip = new HTMLTooltip({doc}, {type: "arrow"});
let tooltip = new HTMLTooltip({doc}, {type: "arrow", useXulWrapper});
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "35px";
tooltip.setContent(div, {width: 200, height: 35});
@ -84,4 +95,4 @@ add_task(function* () {
"The tooltip arrow remains inside the tooltip panel horizontally");
yield hideTooltip(tooltip);
}
});
}

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

@ -34,7 +34,6 @@ function getTooltipContent(doc) {
}
add_task(function* () {
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
let box1 = doc.getElementById("box1");
@ -44,7 +43,7 @@ add_task(function* () {
let width = 100, height = 50;
let tooltip = new HTMLTooltip({doc}, {});
let tooltip = new HTMLTooltip({doc}, {useXulWrapper: false});
tooltip.setContent(getTooltipContent(doc), {width, height});
info("Show the tooltip on each of the 4 hbox, without calling hide in between");

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

@ -30,7 +30,6 @@ 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("Test a tooltip is not closed when clicking inside itself");
@ -40,7 +39,7 @@ add_task(function* () {
let box3 = doc.getElementById("box3");
let box4 = doc.getElementById("box4");
let tooltip = new HTMLTooltip({doc}, {});
let tooltip = new HTMLTooltip({doc}, {useXulWrapper: false});
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "100px";

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

@ -29,6 +29,8 @@ const TOOLTIP_HEIGHT = 50;
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
let useXulWrapper;
add_task(function* () {
// Force the toolbox to be 400px tall => 50px for each box.
yield pushPref("devtools.toolbox.footer.height", 400);
@ -36,7 +38,17 @@ add_task(function* () {
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
let tooltip = new HTMLTooltip({doc}, {});
info("Run tests for a Tooltip without using a XUL panel");
useXulWrapper = false;
yield runTests(doc);
info("Run tests for a Tooltip with a XUL panel");
useXulWrapper = true;
yield runTests(doc);
});
function* runTests(doc) {
let tooltip = new HTMLTooltip({doc}, {useXulWrapper});
info("Set tooltip content 50px tall, but request a container 200px tall");
let tooltipContent = doc.createElementNS(HTML_NS, "div");
tooltipContent.style.cssText = "height: " + TOOLTIP_HEIGHT + "px; background: red;";
@ -71,4 +83,4 @@ add_task(function* () {
EventUtils.synthesizeMouse(tooltip.container, 100, CONTAINER_HEIGHT + 10,
{}, doc.defaultView);
yield onHidden;
});
}

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

@ -25,11 +25,23 @@ const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
loadHelperScript("helper_html_tooltip.js");
let useXulWrapper;
add_task(function* () {
yield addTab("about:blank");
let [,, doc] = yield createHost("bottom", TEST_URI);
let tooltip = new HTMLTooltip({doc}, {});
info("Run tests for a Tooltip without using a XUL panel");
useXulWrapper = false;
yield runTests(doc);
info("Run tests for a Tooltip with a XUL panel");
useXulWrapper = true;
yield runTests(doc);
});
function* runTests(doc) {
let tooltip = new HTMLTooltip({doc}, {useXulWrapper});
info("Create tooltip content width to 150px");
let tooltipContent = doc.createElementNS(HTML_NS, "div");
tooltipContent.style.cssText = "height: 100%; width: 150px; background: red;";
@ -44,4 +56,4 @@ add_task(function* () {
is(panelRect.width, 150, "Tooltip panel has the expected width.");
yield hideTooltip(tooltip);
});
}

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

@ -0,0 +1,78 @@
/* 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 can overflow out of the toolbox when using a XUL panel wrapper.
*/
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/tooltips.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");
// The test toolbox will be 200px tall, the anchors are 50px tall, therefore, the maximum
// tooltip height that could fit in the toolbox is 150px. Setting 160px, the tooltip will
// either have to overflow or to be resized.
const TOOLTIP_HEIGHT = 160;
const TOOLTIP_WIDTH = 200;
add_task(function* () {
// Force the toolbox to be 200px high;
yield pushPref("devtools.toolbox.footer.height", 200);
let [, win, doc] = yield createHost("bottom", TEST_URI);
info("Resizing window to have some space below the window.");
let originalWidth = win.top.outerWidth;
let originalHeight = win.top.outerHeight;
win.top.resizeBy(0, -100);
info("Create HTML tooltip");
let tooltip = new HTMLTooltip({doc}, {useXulWrapper: true});
let div = doc.createElementNS(HTML_NS, "div");
div.style.height = "200px";
div.style.background = "red";
tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
let box1 = doc.getElementById("box1");
// Above box1: check that the tooltip can overflow onto the content page.
info("Display the tooltip above box1.");
yield showTooltip(tooltip, box1, {position: "top"});
checkTooltip(tooltip, "top", TOOLTIP_HEIGHT);
yield hideTooltip(tooltip);
// Below box1: check that the tooltip can overflow out of the browser window.
info("Display the tooltip below box1.");
yield showTooltip(tooltip, box1, {position: "bottom"});
checkTooltip(tooltip, "bottom", TOOLTIP_HEIGHT);
yield hideTooltip(tooltip);
is(tooltip.isVisible(), false, "Tooltip is not visible");
info("Restore original window dimensions.");
win.top.resizeTo(originalWidth, originalHeight);
});
function checkTooltip(tooltip, position, height) {
is(tooltip.position, position, "Actual tooltip position is " + position);
let rect = tooltip.container.getBoundingClientRect();
is(rect.height, height, "Actual tooltip height is " + height);
// Testing the actual left/top offsets is not relevant here as it is handled by the XUL
// panel.
}

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

@ -8,6 +8,8 @@
const EventEmitter = require("devtools/shared/event-emitter");
const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
const {listenOnce} = require("devtools/shared/async-utils");
const {Task} = require("devtools/shared/task");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
@ -49,8 +51,9 @@ const EXTRA_BORDER = {
*
* @param {DOMRect} anchorRect
* Bounding rectangle for the anchor, relative to the tooltip document.
* @param {DOMRect} docRect
* Bounding rectange for the tooltip document owner.
* @param {DOMRect} viewportRect
* Bounding rectangle for the viewport. top/left can be different from 0 if some
* space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
* @param {Number} height
* Preferred height for the tooltip.
* @param {String} pos
@ -61,15 +64,18 @@ const EXTRA_BORDER = {
* - {String} computedPosition: Can differ from the preferred position depending
* on the available height). "top" or "bottom"
*/
const calculateVerticalPosition = function (anchorRect, docRect, height, pos, offset) {
const calculateVerticalPosition =
function (anchorRect, viewportRect, height, pos, offset) {
let {TOP, BOTTOM} = POSITION;
let {top: anchorTop, height: anchorHeight} = anchorRect;
let {bottom: docBottom} = docRect;
// Translate to the available viewport space before calculating dimensions and position.
anchorTop -= viewportRect.top;
// Calculate available space for the tooltip.
let availableTop = anchorTop;
let availableBottom = docBottom - (anchorTop + anchorHeight);
let availableBottom = viewportRect.height - (anchorTop + anchorHeight);
// Find POSITION
let keepPosition = false;
@ -90,6 +96,9 @@ const calculateVerticalPosition = function (anchorRect, docRect, height, pos, of
// Calculate TOP.
let top = pos === TOP ? anchorTop - height - offset : anchorTop + anchorHeight + offset;
// Translate back to absolute coordinates by re-including viewport top margin.
top += viewportRect.top;
return {top, height, computedPosition: pos};
};
@ -100,8 +109,9 @@ const calculateVerticalPosition = function (anchorRect, docRect, height, pos, of
*
* @param {DOMRect} anchorRect
* Bounding rectangle for the anchor, relative to the tooltip document.
* @param {DOMRect} docRect
* Bounding rectange for the tooltip document owner.
* @param {DOMRect} viewportRect
* Bounding rectangle for the viewport. top/left can be different from 0 if some
* space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
* @param {Number} width
* Preferred width for the tooltip.
* @return {Object}
@ -109,18 +119,20 @@ const calculateVerticalPosition = function (anchorRect, docRect, height, pos, of
* - {Number} width: the width to use for the tooltip container.
* - {Number} arrowLeft: the left offset to use for the arrow element.
*/
const calculateHorizontalPosition = function (anchorRect, docRect, width, type, offset) {
const calculateHorizontalPosition =
function (anchorRect, viewportRect, width, type, offset) {
let {left: anchorLeft, width: anchorWidth} = anchorRect;
let {right: docRight} = docRect;
// Translate to the available viewport space before calculating dimensions and position.
anchorLeft -= viewportRect.left;
// Calculate WIDTH.
let availableWidth = docRight;
width = Math.min(width, availableWidth);
width = Math.min(width, viewportRect.width);
// Calculate LEFT.
// By default the tooltip is aligned with the anchor left edge. Unless this
// makes it overflow the viewport, in which case is shifts to the left.
let left = Math.min(anchorLeft + offset, docRight - width);
let left = Math.min(anchorLeft + offset, viewportRect.width - width);
// Calculate ARROW LEFT (tooltip's LEFT might be updated)
let arrowLeft;
@ -141,6 +153,9 @@ const calculateHorizontalPosition = function (anchorRect, docRect, width, type,
arrowLeft = Math.max(arrowLeft, 0);
}
// Translate back to absolute coordinates by re-including viewport left margin.
left += viewportRect.left;
return {left, width, arrowLeft};
};
@ -177,15 +192,25 @@ const getRelativeRect = function (node, relativeTo) {
* - {Boolean} consumeOutsideClicks
* Defaults to true. The tooltip is closed when clicking outside.
* Should this event be stopped and consumed or not.
* - {Boolean} useXulWrapper
* Defaults to true. If the tooltip is hosted in a XUL document, use a XUL panel
* in order to use all the screen viewport available.
*/
function HTMLTooltip(toolbox,
{type = "normal", autofocus = false, consumeOutsideClicks = true} = {}) {
function HTMLTooltip(toolbox, {
type = "normal",
autofocus = false,
consumeOutsideClicks = true,
useXulWrapper = true,
} = {}) {
EventEmitter.decorate(this);
this.doc = toolbox.doc;
this.type = type;
this.autofocus = autofocus;
this.consumeOutsideClicks = consumeOutsideClicks;
this.useXulWrapper = useXulWrapper;
this._position = null;
// Use the topmost window to listen for click events to close the tooltip
this.topWindow = this.doc.defaultView.top;
@ -198,7 +223,20 @@ function HTMLTooltip(toolbox,
this.container = this._createContainer();
if (this._isXUL()) {
if (this._isXUL() && this.useXulWrapper) {
// When using a XUL panel as the wrapper, the actual markup for the tooltip is as
// follows :
// <panel> <!-- XUL panel used to position the tooltip anywhere on screen -->
// <div> <!-- div wrapper used to isolate the tooltip container -->
// <div> <! the actual tooltip.container element -->
this.xulPanelWrapper = this._createXulPanelWrapper();
let inner = this.doc.createElementNS(XHTML_NS, "div");
inner.classList.add("tooltip-xul-wrapper-inner");
this.doc.documentElement.appendChild(this.xulPanelWrapper);
this.xulPanelWrapper.appendChild(inner);
inner.appendChild(this.container);
} else if (this._isXUL()) {
this.doc.documentElement.appendChild(this.container);
} else {
// In non-XUL context the container is ready to use as is.
@ -224,6 +262,13 @@ HTMLTooltip.prototype = {
return this.container.querySelector(".tooltip-arrow");
},
/**
* Retrieve the displayed position used for the tooltip. Null if the tooltip is hidden.
*/
get position() {
return this.isVisible() ? this._position : null;
},
/**
* Set the tooltip content element. The preferred width/height should also be
* specified here.
@ -260,39 +305,53 @@ HTMLTooltip.prototype = {
* - {Number} x: optional, horizontal offset between the anchor and the tooltip
* - {Number} y: optional, vertical offset between the anchor and the tooltip
*/
show: function (anchor, {position, x = 0, y = 0} = {}) {
show: Task.async(function* (anchor, {position, x = 0, y = 0} = {}) {
// Get anchor geometry
let anchorRect = getRelativeRect(anchor, this.doc);
// Get document geometry
let docRect = this.doc.documentElement.getBoundingClientRect();
if (this.useXulWrapper) {
anchorRect = this._convertToScreenRect(anchorRect);
}
// Get viewport size
let viewportRect = this._getViewportRect();
let themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
let preferredHeight = this.preferredHeight + themeHeight;
let {top, height, computedPosition} =
calculateVerticalPosition(anchorRect, docRect, preferredHeight, position, y);
calculateVerticalPosition(anchorRect, viewportRect, preferredHeight, position, y);
// Apply height and top information before measuring the content width (if "auto").
this._position = computedPosition;
// Apply height before measuring the content width (if width="auto").
let isTop = computedPosition === POSITION.TOP;
this.container.classList.toggle("tooltip-top", isTop);
this.container.classList.toggle("tooltip-bottom", !isTop);
this.container.style.height = height + "px";
this.container.style.top = top + "px";
let preferredWidth;
if (this.preferredWidth === "auto") {
preferredWidth = this._measureContainerWidth();
} else {
let themeWidth = 2 * EXTRA_BORDER[this.type];
let preferredWidth = this.preferredWidth === "auto" ?
this._measureContainerWidth() : this.preferredWidth + themeWidth;
preferredWidth = this.preferredWidth + themeWidth;
}
let {left, width, arrowLeft} =
calculateHorizontalPosition(anchorRect, docRect, preferredWidth, this.type, x);
calculateHorizontalPosition(anchorRect, viewportRect, preferredWidth, this.type, x);
this.container.style.width = width + "px";
this.container.style.left = left + "px";
if (this.type === TYPE.ARROW) {
this.arrow.style.left = arrowLeft + "px";
}
if (this.useXulWrapper) {
this._showXulWrapperAt(left, top);
} else {
this.container.style.left = left + "px";
this.container.style.top = top + "px";
}
this.container.classList.add("tooltip-visible");
// Keep a pointer on the focused element to refocus it when hiding the tooltip.
@ -304,14 +363,53 @@ HTMLTooltip.prototype = {
this.topWindow.addEventListener("click", this._onClick, true);
this.emit("shown");
}, 0);
}),
/**
* Calculate the rect of the viewport that limits the tooltip dimensions. When using a
* XUL panel wrapper, the viewport will be able to use the whole screen (excluding space
* reserved by the OS for toolbars etc.). Otherwise, the viewport is limited to the
* tooltip's document.
*
* @return {Object} DOMRect-like object with the Number properties: top, right, bottom,
* left, width, height
*/
_getViewportRect: function () {
if (this.useXulWrapper) {
// availLeft/Top are the coordinates first pixel available on the screen for
// applications (excluding space dedicated for OS toolbars, menus etc...)
// availWidth/Height are the dimensions available to applications excluding all
// the OS reserved space
let {availLeft, availTop, availHeight, availWidth} = this.doc.defaultView.screen;
return {
top: availTop,
right: availLeft + availWidth,
bottom: availTop + availHeight,
left: availLeft,
width: availWidth,
height: availHeight,
};
}
return this.doc.documentElement.getBoundingClientRect();
},
_measureContainerWidth: function () {
let xulParent = this.container.parentNode;
if (this.useXulWrapper && !this.isVisible()) {
// Move the container out of the XUL Panel to measure it.
this.doc.documentElement.appendChild(this.container);
}
this.container.classList.add("tooltip-hidden");
this.container.style.left = "0px";
this.container.style.width = "auto";
let width = this.container.getBoundingClientRect().width;
this.container.classList.remove("tooltip-hidden");
if (this.useXulWrapper && !this.isVisible()) {
xulParent.appendChild(this.container);
}
return width;
},
@ -319,7 +417,7 @@ HTMLTooltip.prototype = {
* Hide the current tooltip. The event "hidden" will be fired when the tooltip
* is hidden.
*/
hide: function () {
hide: Task.async(function* () {
this.doc.defaultView.clearTimeout(this.attachEventsTimer);
if (!this.isVisible()) {
return;
@ -327,6 +425,10 @@ HTMLTooltip.prototype = {
this.topWindow.removeEventListener("click", this._onClick, true);
this.container.classList.remove("tooltip-visible");
if (this.useXulWrapper) {
yield this._hideXulWrapper();
}
this.emit("hidden");
let tooltipHasFocus = this.container.contains(this.doc.activeElement);
@ -334,7 +436,7 @@ HTMLTooltip.prototype = {
this._focusedElement.focus();
this._focusedElement = null;
}
},
}),
/**
* Check if the tooltip is currently displayed.
@ -351,6 +453,9 @@ HTMLTooltip.prototype = {
destroy: function () {
this.hide();
this.container.remove();
if (this.xulPanelWrapper) {
this.xulPanelWrapper.remove();
}
},
_createContainer: function () {
@ -407,13 +512,6 @@ HTMLTooltip.prototype = {
return false;
},
/**
* Check if the tooltip's owner document is a XUL document.
*/
_isXUL: function () {
return this.doc.documentElement.namespaceURI === XUL_NS;
},
/**
* If the tootlip is configured to autofocus and a focusable element can be found,
* focus it.
@ -427,4 +525,52 @@ HTMLTooltip.prototype = {
focusableElement.focus();
}
},
/**
* Check if the tooltip's owner document is a XUL document.
*/
_isXUL: function () {
return this.doc.documentElement.namespaceURI === XUL_NS;
},
_createXulPanelWrapper: function () {
let panel = this.doc.createElementNS(XUL_NS, "panel");
// XUL panel is only a way to display DOM elements outside of the document viewport,
// so disable all features that impact the behavior.
panel.setAttribute("animate", false);
panel.setAttribute("consumeoutsideclicks", false);
panel.setAttribute("noautofocus", true);
panel.setAttribute("ignorekeys", true);
panel.setAttribute("level", "float");
panel.setAttribute("class", "tooltip-xul-wrapper");
return panel;
},
_showXulWrapperAt: function (left, top) {
let onPanelShown = listenOnce(this.xulPanelWrapper, "popupshown");
this.xulPanelWrapper.openPopupAtScreen(left, top, false);
return onPanelShown;
},
_hideXulWrapper: function () {
let onPanelHidden = listenOnce(this.xulPanelWrapper, "popuphidden");
this.xulPanelWrapper.hidePopup();
return onPanelHidden;
},
/**
* Convert from coordinates relative to the tooltip's document, to coordinates relative
* to the "available" screen. By "available" we mean the screen, excluding the OS bars
* display on screen edges.
*/
_convertToScreenRect: function ({left, top, width, height}) {
// mozInnerScreenX/Y are the coordinates of the top left corner of the window's
// viewport, excluding chrome UI.
left += this.doc.defaultView.mozInnerScreenX;
top += this.doc.defaultView.mozInnerScreenY;
return {top, right: left + width, bottom: top + height, left, width, height};
},
};

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

@ -109,6 +109,17 @@
overflow: hidden;
}
.tooltip-xul-wrapper {
-moz-appearance: none;
background: transparent;
overflow: visible;
border-style: none;
}
.tooltip-xul-wrapper .tooltip-container {
position: absolute;
}
.tooltip-top {
flex-direction: column;
}
@ -137,6 +148,11 @@
filter: drop-shadow(0 3px 4px var(--theme-tooltip-shadow));
}
.tooltip-xul-wrapper .tooltip-container[type="arrow"] {
/* When displayed in a XUL panel the drop shadow would be abruptly cut by the panel */
filter: none;
}
.tooltip-container[type="arrow"] > .tooltip-panel {
position: relative;
flex-grow: 0;
@ -265,7 +281,7 @@
.event-tooltip-content-box {
display: none;
height: 54px;
height: 100px;
overflow: hidden;
margin-inline-end: 0;
border: 1px solid var(--theme-splitter-color);