blob: a794c3e635192a5bd07a6a32578781d7d1791fb9 [file] [log] [blame]
// Copyright (c) 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @unrestricted
*/
export default class Tooltip {
/**
* @param {!Document} doc
*/
constructor(doc) {
this.element = doc.body.createChild('div');
this._shadowRoot = UI.createShadowRootWithCoreStyles(this.element, 'ui/tooltip.css');
this._tooltipElement = this._shadowRoot.createChild('div', 'tooltip');
doc.addEventListener('mousemove', this._mouseMove.bind(this), true);
doc.addEventListener('mousedown', this._hide.bind(this, true), true);
doc.addEventListener('mouseleave', this._hide.bind(this, false), true);
doc.addEventListener('keydown', this._hide.bind(this, true), true);
UI.zoomManager.addEventListener(UI.ZoomManager.Events.ZoomChanged, this._reset, this);
doc.defaultView.addEventListener('resize', this._reset.bind(this), false);
}
/**
* @param {!Document} doc
*/
static installHandler(doc) {
new Tooltip(doc);
}
/**
* @param {!Element} element
* @param {?Element|string} tooltipContent
* @param {string=} actionId
* @param {!Object=} options
*/
static install(element, tooltipContent, actionId, options) {
if (!tooltipContent) {
delete element[_symbol];
return;
}
element[_symbol] = {content: tooltipContent, actionId: actionId, options: options || {}};
}
/**
* @param {!Element} element
*/
static addNativeOverrideContainer(element) {
_nativeOverrideContainer.push(element);
}
/**
* @param {!Event} event
*/
_mouseMove(event) {
const mouseEvent = /** @type {!MouseEvent} */ (event);
const path = mouseEvent.composedPath();
if (!path || mouseEvent.buttons !== 0 || (mouseEvent.movementX === 0 && mouseEvent.movementY === 0)) {
return;
}
if (this._anchorElement && path.indexOf(this._anchorElement) === -1) {
this._hide(false);
}
for (const element of path) {
if (element === this._anchorElement) {
return;
}
// The offsetParent is null when the element or an ancestor has 'display: none'.
if (!(element instanceof Element) || element.offsetParent === null) {
continue;
}
if (element[_symbol]) {
this._show(element, mouseEvent);
return;
}
}
}
/**
* @param {!Element} anchorElement
* @param {!Event} event
*/
_show(anchorElement, event) {
const tooltip = anchorElement[_symbol];
this._anchorElement = anchorElement;
this._tooltipElement.removeChildren();
// Check if native tooltips should be used.
for (const element of _nativeOverrideContainer) {
if (this._anchorElement.isSelfOrDescendant(element)) {
Object.defineProperty(this._anchorElement, 'title', /** @type {!Object} */ (_nativeTitle));
this._anchorElement.title = tooltip.content;
return;
}
}
if (typeof tooltip.content === 'string') {
this._tooltipElement.setTextContentTruncatedIfNeeded(tooltip.content);
} else {
this._tooltipElement.appendChild(tooltip.content);
}
if (tooltip.actionId) {
const shortcuts = UI.shortcutRegistry.shortcutDescriptorsForAction(tooltip.actionId);
for (const shortcut of shortcuts) {
const shortcutElement = this._tooltipElement.createChild('div', 'tooltip-shortcut');
shortcutElement.textContent = shortcut.name;
}
}
this._tooltipElement.classList.add('shown');
// Reposition to ensure text doesn't overflow unnecessarily.
this._tooltipElement.positionAt(0, 0);
// Show tooltip instantly if a tooltip was shown recently.
const now = Date.now();
const instant = (this._tooltipLastClosed && now - this._tooltipLastClosed < Timing.InstantThreshold);
this._tooltipElement.classList.toggle('instant', instant);
this._tooltipLastOpened = instant ? now : now + Timing.OpeningDelay;
// Get container element.
const container = UI.GlassPane.container(/** @type {!Document} */ (anchorElement.ownerDocument));
// Position tooltip based on the anchor element.
const containerBox = container.boxInWindow(this.element.window());
const anchorBox = this._anchorElement.boxInWindow(this.element.window());
const anchorOffset = 2;
const pageMargin = 2;
const cursorOffset = 10;
this._tooltipElement.classList.toggle('tooltip-breakword', !this._tooltipElement.textContent.match('\\s'));
this._tooltipElement.style.maxWidth = (containerBox.width - pageMargin * 2) + 'px';
this._tooltipElement.style.maxHeight = '';
const tooltipWidth = this._tooltipElement.offsetWidth;
const tooltipHeight = this._tooltipElement.offsetHeight;
const anchorTooltipAtElement =
this._anchorElement.nodeName === 'BUTTON' || this._anchorElement.nodeName === 'LABEL';
let tooltipX = anchorTooltipAtElement ? anchorBox.x : event.x + cursorOffset;
tooltipX = Number.constrain(
tooltipX, containerBox.x + pageMargin, containerBox.x + containerBox.width - tooltipWidth - pageMargin);
let tooltipY;
if (!anchorTooltipAtElement) {
tooltipY = event.y + cursorOffset + tooltipHeight < containerBox.y + containerBox.height ?
event.y + cursorOffset :
event.y - tooltipHeight - 1;
} else {
const onBottom =
anchorBox.y + anchorOffset + anchorBox.height + tooltipHeight < containerBox.y + containerBox.height;
tooltipY = onBottom ? anchorBox.y + anchorBox.height + anchorOffset : anchorBox.y - tooltipHeight - anchorOffset;
}
this._tooltipElement.positionAt(tooltipX, tooltipY);
}
/**
* @param {boolean} removeInstant
*/
_hide(removeInstant) {
delete this._anchorElement;
this._tooltipElement.classList.remove('shown');
if (Date.now() > this._tooltipLastOpened) {
this._tooltipLastClosed = Date.now();
}
if (removeInstant) {
delete this._tooltipLastClosed;
}
}
_reset() {
this._hide(true);
this._tooltipElement.positionAt(0, 0);
this._tooltipElement.style.maxWidth = '0';
this._tooltipElement.style.maxHeight = '0';
}
}
const Timing = {
// Max time between tooltips showing that no opening delay is required.
'InstantThreshold': 300,
// Wait time before opening a tooltip.
'OpeningDelay': 600
};
const _symbol = Symbol('Tooltip');
/** @type {!Array.<!Element>} */
const _nativeOverrideContainer = [];
const _nativeTitle = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'title');
Object.defineProperty(HTMLElement.prototype, 'title', {
/**
* @return {!Element|string}
* @this {!Element}
*/
get: function() {
const tooltip = this[UI.Tooltip._symbol];
return tooltip ? tooltip.content : '';
},
/**
* @param {!Element|string} x
* @this {!Element}
*/
set: function(x) {
Tooltip.install(this, x);
}
});
/* Legacy exported object*/
self.UI = self.UI || {};
/* Legacy exported object*/
UI = UI || {};
/** @constructor */
UI.Tooltip = Tooltip;
UI.Tooltip._symbol = _symbol;