/* 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/. */
const ARROW_WIDTH = {
normal: 0,
arrow: 32, // This is the value calculated for the .tooltip-arrow element in tooltip.css // which includes the arrow width (20px) plus the extra margin added so that // the drop shadow is not cropped (2px each side).
doorhanger: 24,
};
const ARROW_OFFSET = {
normal: 0, // Default offset between the tooltip's edge and the tooltip arrow.
arrow: 20, // Match other Firefox menus which use 10px from edge (but subtract the 2px // margin included in the ARROW_WIDTH above).
doorhanger: 8,
};
const EXTRA_HEIGHT = {
normal: 0, // The arrow is 16px tall, but merges on with the panel border
arrow: 14, // The doorhanger arrow is 10px tall, but merges on 1px with the panel border
doorhanger: 9,
};
/** * Calculate the vertical position & offsets to use for the tooltip. Will attempt to * respect the provided height and position preferences, unless the available height * prevents this. * * @param {DOMRect} anchorRect * Bounding rectangle for the anchor, relative to the tooltip document. * @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 * Preferred position for the tooltip. Possible values: "top" or "bottom". * @param {Number} offset * Offset between the top of the anchor and the tooltip. * @return {Object} * - {Number} top: the top offset for the tooltip. * - {Number} height: the height to use for the tooltip container. * - {String} computedPosition: Can differ from the preferred position depending * on the available height). "top" or "bottom"
*/ const calculateVerticalPosition = (
anchorRect,
viewportRect,
height,
pos,
offset
) => { const { TOP, BOTTOM } = POSITION;
let { top: anchorTop, height: anchorHeight } = anchorRect;
// Translate to the available viewport space before calculating dimensions and position.
anchorTop -= viewportRect.top;
// Calculate available space for the tooltip. const availableTop = anchorTop; const availableBottom = viewportRect.height - (anchorTop + anchorHeight);
// Find POSITION
let keepPosition = false; if (pos === TOP) {
keepPosition = availableTop >= height + offset;
} elseif (pos === BOTTOM) {
keepPosition = availableBottom >= height + offset;
} if (!keepPosition) {
pos = availableTop > availableBottom ? TOP : BOTTOM;
}
/** * Calculate the horizontal position & offsets to use for the tooltip. Will * attempt to respect the provided width and position preferences, unless the * available width prevents this. * * @param {DOMRect} anchorRect * Bounding rectangle for the anchor, relative to the tooltip document. * @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 {DOMRect} windowRect * Bounding rectangle for the window. Used to determine which direction * doorhangers should hang. * @param {Number} width * Preferred width for the tooltip. * @param {String} type * The tooltip type (e.g. "arrow"). * @param {Number} offset * Horizontal offset in pixels. * @param {Number} borderRadius * The border radius of the panel. This is added to ARROW_OFFSET to * calculate the distance from the edge of the tooltip to the start * of arrow. It is separate from ARROW_OFFSET since it will vary by * platform. * @param {Boolean} isRtl * If the anchor is in RTL, the tooltip should be aligned to the right. * @return {Object} * - {Number} left: the left offset for the tooltip. * - {Number} width: the width to use for the tooltip container. * - {Number} arrowLeft: the left offset to use for the arrow element.
*/ const calculateHorizontalPosition = (
anchorRect,
viewportRect,
windowRect,
width,
type,
offset,
borderRadius,
isRtl,
isMenuTooltip
) => { // All tooltips from content should follow the writing direction. // // For tooltips (including doorhanger tooltips) we follow the writing // direction but for menus created using doorhangers the guidelines[1] say // that: // // "Doorhangers opening on the right side of the view show the directional // arrow on the right. // // Doorhangers opening on the left side of the view show the directional // arrow on the left. // // Never place the directional arrow at the center of doorhangers." // // [1] https://design.firefox.com/photon/components/doorhangers.html#directional-arrow // // So for those we need to check if the anchor is more right or left.
let hangDirection; if (type === TYPE.DOORHANGER && isMenuTooltip) { const anchorCenter = anchorRect.left + anchorRect.width / 2; const viewCenter = windowRect.left + windowRect.width / 2;
hangDirection = anchorCenter >= viewCenter ? "left" : "right";
} else {
hangDirection = isRtl ? "left" : "right";
}
const anchorWidth = anchorRect.width;
// Calculate logical start of anchor relative to the viewport. const anchorStart =
hangDirection === "right"
? anchorRect.left - viewportRect.left
: viewportRect.right - anchorRect.right;
// Calculate arrow start (tooltip's start might be updated) const arrowWidth = ARROW_WIDTH[type];
let arrowStart; // Arrow and doorhanger style tooltips may need to be shifted if (type === TYPE.ARROW || type === TYPE.DOORHANGER) { const arrowOffset = ARROW_OFFSET[type] + borderRadius;
// Where will the point of the arrow be if we apply the standard offset? const arrowCenter = tooltipStart + arrowOffset + arrowWidth / 2;
// How does that compare to the center of the anchor? const anchorCenter = anchorStart + anchorWidth / 2;
// If the anchor is too narrow, align the arrow and the anchor center. if (arrowCenter > anchorCenter) {
tooltipStart = Math.max(0, tooltipStart - (arrowCenter - anchorCenter));
} // Arrow's start offset relative to the anchor.
arrowStart = Math.min(arrowOffset, (anchorWidth - arrowWidth) / 2) | 0; // Translate the coordinate to tooltip container
arrowStart += anchorStart - tooltipStart; // Make sure the arrow remains in the tooltip container.
arrowStart = Math.min(arrowStart, tooltipWidth - arrowWidth - borderRadius);
arrowStart = Math.max(arrowStart, borderRadius);
}
/** * 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).
*/ const getRelativeRect = function (node, relativeTo) { // getBoxQuads is a non-standard WebAPI which will not work on non-firefox // browser when running launchpad on Chrome. if (
!node.getBoxQuads ||
!node.getBoxQuads({
relativeTo,
createFramesForSuppressedWhitespace: false,
})[0]
) { const { top, left, width, height } = node.getBoundingClientRect(); const right = left + width; const bottom = top + height; return { top, right, bottom, left, width, height };
}
// Width and Height can be taken from the rect. const { width, height } = node.getBoundingClientRect();
const quadBounds = node
.getBoxQuads({ relativeTo, createFramesForSuppressedWhitespace: false })[0]
.getBounds(); const top = quadBounds.top; const left = quadBounds.left;
// Compute right and bottom coordinates using the rest of the data. const right = left + width; const bottom = top + height;
/** * The HTMLTooltip can display HTML content in a tooltip popup. * * @param {Document} toolboxDoc * The toolbox document to attach the HTMLTooltip popup. * @param {Object} * - {String} className * A string separated list of classes to add to the tooltip container * element. * - {Boolean} consumeOutsideClicks * Defaults to true. The tooltip is closed when clicking outside. * Should this event be stopped and consumed or not. * - {String} id * The ID to assign to the tooltip container element. * - {Boolean} isMenuTooltip * Defaults to false. If the tooltip is a menu then this should be set * to true. * - {String} type * Display type of the tooltip. Possible values: "normal", "arrow", and * "doorhanger". * - {Boolean} useXulWrapper * Defaults to false. If the tooltip is hosted in a XUL document, use a * XUL panel in order to use all the screen viewport available. * - {Boolean} noAutoHide * Defaults to false. If this property is set to false or omitted, the * tooltip will automatically disappear after a few seconds. If this * attribute is set to true, this will not happen and the tooltip will * only hide when the user moves the mouse to another element.
*/ function HTMLTooltip(
toolboxDoc,
{
className = "",
consumeOutsideClicks = true,
id = "",
isMenuTooltip = false,
type = "normal",
useXulWrapper = false,
noAutoHide = false,
} = {}
) {
EventEmitter.decorate(this);
this.doc = toolboxDoc; this.id = id; this.className = className; this.type = type; this.noAutoHide = noAutoHide; // consumeOutsideClicks cannot be used if the tooltip is not closed on click this.consumeOutsideClicks = this.noAutoHide ? false : consumeOutsideClicks; this.isMenuTooltip = isMenuTooltip; this.useXulWrapper = this._isXULPopupAvailable() && useXulWrapper; this.preferredWidth = "auto"; this.preferredHeight = "auto";
// The top window is used to attach click event listeners to close the tooltip if the // user clicks on the content page. this.topWindow = this._getTopWindow();
this.container = this._createContainer(); if (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> <! the actual tooltip-container element --> this.xulPanelWrapper = this._createXulPanelWrapper(); this.doc.documentElement.appendChild(this.xulPanelWrapper); this.xulPanelWrapper.appendChild(this.container);
} elseif (this._hasXULRootElement()) { this.doc.documentElement.appendChild(this.container);
} else { // In non-XUL context the container is ready to use as is. this.doc.body.appendChild(this.container);
}
}
module.exports.HTMLTooltip = HTMLTooltip;
HTMLTooltip.prototype = { /** * The tooltip panel is the parentNode of the tooltip content.
*/
get panel() { returnthis.container.querySelector(".tooltip-panel");
},
/** * The arrow element. Might be null depending on the tooltip type.
*/
get arrow() { returnthis.container.querySelector(".tooltip-arrow");
},
/** * Retrieve the displayed position used for the tooltip. Null if the tooltip is hidden.
*/
get position() { returnthis.isVisible() ? this._position : null;
},
get toggle() { if (!this._toggle) { this._toggle = new TooltipToggle(this);
}
returnthis._toggle;
},
/** * Set the preferred width/height of the panel content. * The panel content is set by appending content to `this.panel`. * * @param {Object} * - {Number} width: preferred width for the tooltip container. If not specified * the tooltip container will be measured before being displayed, and the * measured width will be used as the preferred width. * - {Number} height: preferred height for the tooltip container. If * not specified the tooltip container will be measured before being * displayed, and the measured height will be used as the preferred * height. * * For tooltips whose content height may change while being * displayed, the special value Infinity may be used to produce * a flexible container that accommodates resizing content. Note, * however, that when used in combination with the XUL wrapper the * unfilled part of this container will consume all mouse events * making content behind this area inaccessible until the tooltip is * dismissed.
*/
setContentSize({ width = "auto", height = "auto" } = {}) { this.preferredWidth = width; this.preferredHeight = height;
},
/** * Show the tooltip next to the provided anchor element, or update the tooltip position * if it was already visible. 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} options * Optional settings for positioning the tooltip. * @param {String} options.position * Optional, possible values: top|bottom * If layout permits, the tooltip will be displayed on top/bottom * of the anchor. If omitted, the tooltip will be displayed where * more space is available. * @param {Number} options.x * Optional, horizontal offset between the anchor and the tooltip. * @param {Number} options.y * Optional, vertical offset between the anchor and the tooltip.
*/
async show(anchor, options) { const { left, top } = this._updateContainerBounds(anchor, options); const isTooltipVisible = this.isVisible();
if (this.useXulWrapper) { if (!isTooltipVisible) {
await this._showXulWrapperAt(left, top);
} else { this._moveXulWrapperTo(left, top);
}
} else { this.container.style.left = left + "px"; this.container.style.top = top + "px";
}
if (isTooltipVisible) { return;
}
this.container.classList.add("tooltip-visible");
// Keep a pointer on the focused element to refocus it when hiding the tooltip. this._focusedElement = anchor.ownerDocument.activeElement;
if (this.doc.defaultView) { if (!this._pendingEventListenerPromise) { // On Windows and Linux, if the tooltip is shown on mousedown/click (which is the // case for the MenuButton component for example), attaching the events listeners // on the window right away would trigger the callbacks; which means the tooltip // would be instantly hidden. To prevent such thing, the event listeners are set // on the next tick. this._pendingEventListenerPromise = new Promise(resolve => { this.doc.defaultView.setTimeout(() => { // Update the top window reference each time in case the host changes. this.topWindow = this._getTopWindow(); this.topWindow.addEventListener("click", this._onClick, true); this.topWindow.addEventListener("mouseup", this._onMouseup, true);
resolve();
}, 0);
});
}
// This is redundant with tooltip-visible, and tooltip-visible // should only be added from here, after the click listener is set. // Otherwise, code listening to tooltip-visible may be firing a click that would be lost. // Unfortunately, doing this cause many non trivial test failures. this.container.classList.add("tooltip-shown");
// Calculate the horizontal position and width
let preferredWidth; // Record the height too since it might save us from having to look it up // later.
let measuredHeight; const currentScrollTop = this.panel.scrollTop; if (this.preferredWidth === "auto") { // Reset any styles that constrain the dimensions we want to calculate. this.container.style.width = "auto"; if (this.preferredHeight === "auto") { this.container.style.height = "auto";
}
({ width: preferredWidth, height: measuredHeight } = this._measureContainerSize());
} else {
preferredWidth = this.preferredWidth;
}
// If we constrained the width, then any measured height we have is no // longer valid. if (measuredHeight && width !== preferredWidth) {
measuredHeight = undefined;
}
// Work out how much vertical margin we have. // // This relies on us having set either .tooltip-top or .tooltip-bottom // and on the margins for both being symmetrical. Fortunately the call to // _measureContainerSize above will set .tooltip-top for us and it also // assumes these styles are symmetrical so this should be ok. const panelWindow = this.panel.ownerDocument.defaultView; const panelComputedStyle = panelWindow.getComputedStyle(this.panel); const verticalMargin =
parseFloat(panelComputedStyle.marginTop) +
parseFloat(panelComputedStyle.marginBottom);
// Calculate the vertical position and height
let preferredHeight; if (this.preferredHeight === "auto") { if (measuredHeight) { // We already have a valid height measured in a previous step.
preferredHeight = measuredHeight;
} else { this.container.style.height = "auto";
({ height: preferredHeight } = this._measureContainerSize());
}
preferredHeight += verticalMargin;
} else { const themeHeight = EXTRA_HEIGHT[this.type] + verticalMargin;
preferredHeight = this.preferredHeight + themeHeight;
}
// If the preferred height is set to Infinity, the tooltip container should grow based // on its content's height and use as much height as possible. this.container.classList.toggle( "tooltip-flexible-height", this.preferredHeight === Infinity
);
/** * Calculate the following boundary rectangles: * * - Viewport rect: This is the region that limits the tooltip dimensions. * When using a XUL panel wrapper, the tooltip will be able to use the whole * screen (excluding space reserved by the OS for toolbars etc.) and hence * the result will be in screen coordinates. * Otherwise, the tooltip is limited to the tooltip's document. * * - Window rect: This is the bounds of the view in which the tooltip is * presented. It is reported in the same coordinates as the viewport * rect and is used for determining in which direction a doorhanger-type * tooltip should "hang". * When using the XUL panel wrapper this will be the dimensions of the * window in screen coordinates. Otherwise it will be the same as the * viewport rect. * * @param {Object} anchorRect * DOMRect-like object of the target anchor element. * We need to pass this to detect the case when the anchor is not in * the current window (because, the center of the window is in * a different window to the anchor). * * @return {Object} An object with the following properties * viewportRect {Object} DOMRect-like object with the Number * properties: top, right, bottom, left, width, height * representing the viewport rect. * windowRect {Object} DOMRect-like object with the Number * properties: top, right, bottom, left, width, height * representing the window rect.
*/
_getBoundingRects(anchorRect) {
let viewportRect;
let windowRect;
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 const { availLeft, availTop, availHeight, availWidth } = this.doc.defaultView.screen;
viewportRect = {
top: availTop,
right: availLeft + availWidth,
bottom: availTop + availHeight,
left: availLeft,
width: availWidth,
height: availHeight,
};
// If the anchor is outside the viewport, it possibly means we have a // multi-monitor environment where the anchor is displayed on a different // monitor to the "current" screen (as determined by the center of the // window). This can happen when, for example, the screen is spread across // two monitors. // // In this case we simply expand viewport in the direction of the anchor // so that we can still calculate the popup position correctly. if (anchorRect.left > viewportRect.right) { const diffWidth = windowRect.right - viewportRect.right;
viewportRect.right += diffWidth;
viewportRect.width += diffWidth;
} if (anchorRect.right < viewportRect.left) { const diffWidth = viewportRect.left - windowRect.left;
viewportRect.left -= diffWidth;
viewportRect.width += diffWidth;
}
} else {
viewportRect = windowRect = this.doc.documentElement.getBoundingClientRect();
}
return { viewportRect, windowRect };
},
_measureContainerSize() { const 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"); // Set either of the tooltip-top or tooltip-bottom styles so that we get an // accurate height. We're assuming that the two styles will be symmetrical // and that we will clear this as necessary later. this.container.classList.add("tooltip-top"); this.container.classList.remove("tooltip-bottom"); const { width, height } = this.container.getBoundingClientRect(); this.container.classList.remove("tooltip-hidden");
if (this.useXulWrapper && !this.isVisible()) {
xulParent.appendChild(this.container);
}
return { width, height };
},
/** * Hide the current tooltip. The event "hidden" will be fired when the tooltip * is hidden.
*/
async hide({ fromMouseup = false } = {}) { // Exit if the disable autohide setting is in effect or if hide() is called // from a mouseup event and the tooltip has noAutoHide set to true. if (
Services.prefs.getBoolPref("devtools.popup.disable_autohide", false) ||
(this.noAutoHide && this.isVisible() && fromMouseup)
) { return;
}
if (!this.isVisible()) { this.emit("hidden"); return;
}
// If the tooltip is hidden from a mouseup event, wait for a potential click event // to be consumed before removing event listeners. if (fromMouseup) {
await new Promise(resolve => this.topWindow.setTimeout(resolve, 0));
}
/** * Check if the tooltip is currently displayed. * @return {Boolean} true if the tooltip is visible
*/
isVisible() { returnthis.container.classList.contains("tooltip-visible");
},
/** * Destroy the tooltip instance. Hide the tooltip if displayed, remove the * tooltip container from the document.
*/
destroy() { this.hide(); this.removeEventListeners(); this.container.remove(); if (this.xulPanelWrapper) { this.xulPanelWrapper.remove();
} if (this._toggle) { this._toggle.destroy(); this._toggle = null;
}
},
_onClick(e) { if (this._isInTooltipContainer(e.target)) { return;
}
if (this.consumeOutsideClicks && e.button === 0) { // Consume only left click events (button === 0).
e.preventDefault();
e.stopPropagation();
}
},
/** * Hide the tooltip on mouseup rather than on click because the surrounding markup * may change on mousedown in a way that prevents a "click" event from being fired. * If the element that received the mousedown and the mouseup are different, click * will not be fired.
*/
_onMouseup(e) { if (this._isInTooltipContainer(e.target)) { return;
}
this.hide({ fromMouseup: true });
},
_isInTooltipContainer(node) { // Check if the target is the tooltip arrow. if (this.arrow && this.arrow === node) { returntrue;
}
if (typeof node.closest == "function" && node.closest("menupopup")) { // Ignore events from menupopup elements which will not be children of the // tooltip container even if their owner element is in the tooltip. // See Bug 1811002. returntrue;
}
const tooltipWindow = this.panel.ownerDocument.defaultView;
let win = node.ownerDocument.defaultView;
// Check if the tooltip panel contains the node if they live in the same document. if (win === tooltipWindow) { returnthis.panel.contains(node);
}
// Check if the node window is in the tooltip container. while (win.parent && win.parent !== win) { if (win.parent === tooltipWindow) { // If the parent window is the tooltip window, check if the tooltip contains // the current frame element. returnthis.panel.contains(win.frameElement);
}
win = win.parent;
}
returnfalse;
},
_onXulPanelHidden() { if (this.isVisible()) { this.hide();
}
},
/** * Focus on the first focusable item in the tooltip. * * Returns true if we found something to focus on, false otherwise.
*/
focus() { const focusableElement = this.panel.querySelector(focusableSelector); if (focusableElement) {
focusableElement.focus();
} return !!focusableElement;
},
/** * Focus on the last focusable item in the tooltip. * * Returns true if we found something to focus on, false otherwise.
*/
focusEnd() { const focusableElements = this.panel.querySelectorAll(focusableSelector); if (focusableElements.length) {
focusableElements[focusableElements.length - 1].focus();
} return focusableElements.length !== 0;
},
// 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("incontentshell", false);
panel.setAttribute("noautofocus", true);
panel.setAttribute("noautohide", this.noAutoHide);
_moveXulWrapperTo(left, top) { // FIXME: moveTo should probably account for margins when called from // script. Our current shadow set-up only supports one margin, so it's fine // to use the margin top in both directions. const margin = parseFloat( this.xulPanelWrapper.ownerGlobal.getComputedStyle(this.xulPanelWrapper)
.marginTop
); this.xulPanelWrapper.moveTo(left + margin, top + margin);
},
/** * 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({ 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,
};
},
};
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.