| // 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; |