mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
694 lines
25 KiB
TypeScript
694 lines
25 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import 'vs/css!./hover';
|
|
import { DisposableStore } from 'vs/base/common/lifecycle';
|
|
import { Event, Emitter } from 'vs/base/common/event';
|
|
import * as dom from 'vs/base/browser/dom';
|
|
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
|
import { KeyCode } from 'vs/base/common/keyCodes';
|
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
|
import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
|
import { HoverAction, HoverPosition, HoverWidget as BaseHoverWidget, getHoverAccessibleViewHint } from 'vs/base/browser/ui/hover/hoverWidget';
|
|
import { Widget } from 'vs/base/browser/ui/widget';
|
|
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
|
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
import { MarkdownRenderer, openLinkFromMarkdown } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer';
|
|
import { isMarkdownString } from 'vs/base/common/htmlContent';
|
|
import { localize } from 'vs/nls';
|
|
import { isMacintosh } from 'vs/base/common/platform';
|
|
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
|
import { status } from 'vs/base/browser/ui/aria/aria';
|
|
import type { IHoverOptions, IHoverTarget, IHoverWidget } from 'vs/base/browser/ui/hover/hover';
|
|
|
|
const $ = dom.$;
|
|
type TargetRect = {
|
|
left: number;
|
|
right: number;
|
|
top: number;
|
|
bottom: number;
|
|
width: number;
|
|
height: number;
|
|
center: { x: number; y: number };
|
|
};
|
|
|
|
const enum Constants {
|
|
PointerSize = 3,
|
|
HoverBorderWidth = 2,
|
|
HoverWindowEdgeMargin = 2,
|
|
}
|
|
|
|
export class HoverWidget extends Widget implements IHoverWidget {
|
|
private readonly _messageListeners = new DisposableStore();
|
|
private readonly _lockMouseTracker: CompositeMouseTracker;
|
|
|
|
private readonly _hover: BaseHoverWidget;
|
|
private readonly _hoverPointer: HTMLElement | undefined;
|
|
private readonly _hoverContainer: HTMLElement;
|
|
private readonly _target: IHoverTarget;
|
|
private readonly _linkHandler: (url: string) => any;
|
|
|
|
private _isDisposed: boolean = false;
|
|
private _hoverPosition: HoverPosition;
|
|
private _forcePosition: boolean = false;
|
|
private _x: number = 0;
|
|
private _y: number = 0;
|
|
private _isLocked: boolean = false;
|
|
private _enableFocusTraps: boolean = false;
|
|
private _addedFocusTrap: boolean = false;
|
|
|
|
private get _targetWindow(): Window {
|
|
return dom.getWindow(this._target.targetElements[0]);
|
|
}
|
|
private get _targetDocumentElement(): HTMLElement {
|
|
return dom.getWindow(this._target.targetElements[0]).document.documentElement;
|
|
}
|
|
|
|
get isDisposed(): boolean { return this._isDisposed; }
|
|
get isMouseIn(): boolean { return this._lockMouseTracker.isMouseIn; }
|
|
get domNode(): HTMLElement { return this._hover.containerDomNode; }
|
|
|
|
private readonly _onDispose = this._register(new Emitter<void>());
|
|
get onDispose(): Event<void> { return this._onDispose.event; }
|
|
private readonly _onRequestLayout = this._register(new Emitter<void>());
|
|
get onRequestLayout(): Event<void> { return this._onRequestLayout.event; }
|
|
|
|
get anchor(): AnchorPosition { return this._hoverPosition === HoverPosition.BELOW ? AnchorPosition.BELOW : AnchorPosition.ABOVE; }
|
|
get x(): number { return this._x; }
|
|
get y(): number { return this._y; }
|
|
|
|
/**
|
|
* Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not
|
|
* hide and can be hovered regardless of whether the `hideOnHover` hover option is set.
|
|
*/
|
|
get isLocked(): boolean { return this._isLocked; }
|
|
set isLocked(value: boolean) {
|
|
if (this._isLocked === value) {
|
|
return;
|
|
}
|
|
this._isLocked = value;
|
|
this._hoverContainer.classList.toggle('locked', this._isLocked);
|
|
}
|
|
|
|
constructor(
|
|
options: IHoverOptions,
|
|
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
|
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
|
@IOpenerService private readonly _openerService: IOpenerService,
|
|
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
|
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
|
|
) {
|
|
super();
|
|
|
|
this._linkHandler = options.linkHandler || (url => {
|
|
return openLinkFromMarkdown(this._openerService, url, isMarkdownString(options.content) ? options.content.isTrusted : undefined);
|
|
});
|
|
|
|
this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target);
|
|
|
|
this._hoverPointer = options.appearance?.showPointer ? $('div.workbench-hover-pointer') : undefined;
|
|
this._hover = this._register(new BaseHoverWidget());
|
|
this._hover.containerDomNode.classList.add('workbench-hover', 'fadeIn');
|
|
if (options.appearance?.compact) {
|
|
this._hover.containerDomNode.classList.add('workbench-hover', 'compact');
|
|
}
|
|
if (options.appearance?.skipFadeInAnimation) {
|
|
this._hover.containerDomNode.classList.add('skip-fade-in');
|
|
}
|
|
if (options.additionalClasses) {
|
|
this._hover.containerDomNode.classList.add(...options.additionalClasses);
|
|
}
|
|
if (options.position?.forcePosition) {
|
|
this._forcePosition = true;
|
|
}
|
|
if (options.trapFocus) {
|
|
this._enableFocusTraps = true;
|
|
}
|
|
|
|
this._hoverPosition = options.position?.hoverPosition ?? HoverPosition.ABOVE;
|
|
|
|
// Don't allow mousedown out of the widget, otherwise preventDefault will call and text will
|
|
// not be selected.
|
|
this.onmousedown(this._hover.containerDomNode, e => e.stopPropagation());
|
|
|
|
// Hide hover on escape
|
|
this.onkeydown(this._hover.containerDomNode, e => {
|
|
if (e.equals(KeyCode.Escape)) {
|
|
this.dispose();
|
|
}
|
|
});
|
|
|
|
// Hide when the window loses focus
|
|
this._register(dom.addDisposableListener(this._targetWindow, 'blur', () => this.dispose()));
|
|
|
|
const rowElement = $('div.hover-row.markdown-hover');
|
|
const contentsElement = $('div.hover-contents');
|
|
if (typeof options.content === 'string') {
|
|
contentsElement.textContent = options.content;
|
|
contentsElement.style.whiteSpace = 'pre-wrap';
|
|
|
|
} else if (dom.isHTMLElement(options.content)) {
|
|
contentsElement.appendChild(options.content);
|
|
contentsElement.classList.add('html-hover-contents');
|
|
|
|
} else {
|
|
const markdown = options.content;
|
|
const mdRenderer = this._instantiationService.createInstance(
|
|
MarkdownRenderer,
|
|
{ codeBlockFontFamily: this._configurationService.getValue<IEditorOptions>('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily }
|
|
);
|
|
|
|
const { element } = mdRenderer.render(markdown, {
|
|
actionHandler: {
|
|
callback: (content) => this._linkHandler(content),
|
|
disposables: this._messageListeners
|
|
},
|
|
asyncRenderCallback: () => {
|
|
contentsElement.classList.add('code-hover-contents');
|
|
this.layout();
|
|
// This changes the dimensions of the hover so trigger a layout
|
|
this._onRequestLayout.fire();
|
|
}
|
|
});
|
|
contentsElement.appendChild(element);
|
|
}
|
|
rowElement.appendChild(contentsElement);
|
|
this._hover.contentsDomNode.appendChild(rowElement);
|
|
|
|
if (options.actions && options.actions.length > 0) {
|
|
const statusBarElement = $('div.hover-row.status-bar');
|
|
const actionsElement = $('div.actions');
|
|
options.actions.forEach(action => {
|
|
const keybinding = this._keybindingService.lookupKeybinding(action.commandId);
|
|
const keybindingLabel = keybinding ? keybinding.getLabel() : null;
|
|
HoverAction.render(actionsElement, {
|
|
label: action.label,
|
|
commandId: action.commandId,
|
|
run: e => {
|
|
action.run(e);
|
|
this.dispose();
|
|
},
|
|
iconClass: action.iconClass
|
|
}, keybindingLabel);
|
|
});
|
|
statusBarElement.appendChild(actionsElement);
|
|
this._hover.containerDomNode.appendChild(statusBarElement);
|
|
}
|
|
|
|
this._hoverContainer = $('div.workbench-hover-container');
|
|
if (this._hoverPointer) {
|
|
this._hoverContainer.appendChild(this._hoverPointer);
|
|
}
|
|
this._hoverContainer.appendChild(this._hover.containerDomNode);
|
|
|
|
// Determine whether to hide on hover
|
|
let hideOnHover: boolean;
|
|
if (options.actions && options.actions.length > 0) {
|
|
// If there are actions, require hover so they can be accessed
|
|
hideOnHover = false;
|
|
} else {
|
|
if (options.persistence?.hideOnHover === undefined) {
|
|
// When unset, will default to true when it's a string or when it's markdown that
|
|
// appears to have a link using a naive check for '](' and '</a>'
|
|
hideOnHover = typeof options.content === 'string' ||
|
|
isMarkdownString(options.content) && !options.content.value.includes('](') && !options.content.value.includes('</a>');
|
|
} else {
|
|
// It's set explicitly
|
|
hideOnHover = options.persistence.hideOnHover;
|
|
}
|
|
}
|
|
|
|
// Show the hover hint if needed
|
|
if (options.appearance?.showHoverHint) {
|
|
const statusBarElement = $('div.hover-row.status-bar');
|
|
const infoElement = $('div.info');
|
|
infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt');
|
|
statusBarElement.appendChild(infoElement);
|
|
this._hover.containerDomNode.appendChild(statusBarElement);
|
|
}
|
|
|
|
const mouseTrackerTargets = [...this._target.targetElements];
|
|
if (!hideOnHover) {
|
|
mouseTrackerTargets.push(this._hoverContainer);
|
|
}
|
|
const mouseTracker = this._register(new CompositeMouseTracker(mouseTrackerTargets));
|
|
this._register(mouseTracker.onMouseOut(() => {
|
|
if (!this._isLocked) {
|
|
this.dispose();
|
|
}
|
|
}));
|
|
|
|
// Setup another mouse tracker when hideOnHover is set in order to track the hover as well
|
|
// when it is locked. This ensures the hover will hide on mouseout after alt has been
|
|
// released to unlock the element.
|
|
if (hideOnHover) {
|
|
const mouseTracker2Targets = [...this._target.targetElements, this._hoverContainer];
|
|
this._lockMouseTracker = this._register(new CompositeMouseTracker(mouseTracker2Targets));
|
|
this._register(this._lockMouseTracker.onMouseOut(() => {
|
|
if (!this._isLocked) {
|
|
this.dispose();
|
|
}
|
|
}));
|
|
} else {
|
|
this._lockMouseTracker = mouseTracker;
|
|
}
|
|
}
|
|
|
|
private addFocusTrap() {
|
|
if (!this._enableFocusTraps || this._addedFocusTrap) {
|
|
return;
|
|
}
|
|
this._addedFocusTrap = true;
|
|
|
|
// Add a hover tab loop if the hover has at least one element with a valid tabIndex
|
|
const firstContainerFocusElement = this._hover.containerDomNode;
|
|
const lastContainerFocusElement = this.findLastFocusableChild(this._hover.containerDomNode);
|
|
if (lastContainerFocusElement) {
|
|
const beforeContainerFocusElement = dom.prepend(this._hoverContainer, $('div'));
|
|
const afterContainerFocusElement = dom.append(this._hoverContainer, $('div'));
|
|
beforeContainerFocusElement.tabIndex = 0;
|
|
afterContainerFocusElement.tabIndex = 0;
|
|
this._register(dom.addDisposableListener(afterContainerFocusElement, 'focus', (e) => {
|
|
firstContainerFocusElement.focus();
|
|
e.preventDefault();
|
|
}));
|
|
this._register(dom.addDisposableListener(beforeContainerFocusElement, 'focus', (e) => {
|
|
lastContainerFocusElement.focus();
|
|
e.preventDefault();
|
|
}));
|
|
}
|
|
}
|
|
|
|
private findLastFocusableChild(root: Node): HTMLElement | undefined {
|
|
if (root.hasChildNodes()) {
|
|
for (let i = 0; i < root.childNodes.length; i++) {
|
|
const node = root.childNodes.item(root.childNodes.length - i - 1);
|
|
if (node.nodeType === node.ELEMENT_NODE) {
|
|
const parsedNode = node as HTMLElement;
|
|
if (typeof parsedNode.tabIndex === 'number' && parsedNode.tabIndex >= 0) {
|
|
return parsedNode;
|
|
}
|
|
}
|
|
const recursivelyFoundElement = this.findLastFocusableChild(node);
|
|
if (recursivelyFoundElement) {
|
|
return recursivelyFoundElement;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
public render(container: HTMLElement): void {
|
|
container.appendChild(this._hoverContainer);
|
|
const hoverFocused = this._hoverContainer.contains(this._hoverContainer.ownerDocument.activeElement);
|
|
const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint(this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(), this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel());
|
|
if (accessibleViewHint) {
|
|
|
|
status(accessibleViewHint);
|
|
}
|
|
this.layout();
|
|
this.addFocusTrap();
|
|
}
|
|
|
|
public layout() {
|
|
this._hover.containerDomNode.classList.remove('right-aligned');
|
|
this._hover.contentsDomNode.style.maxHeight = '';
|
|
|
|
const getZoomAccountedBoundingClientRect = (e: HTMLElement) => {
|
|
const zoom = dom.getDomNodeZoomLevel(e);
|
|
|
|
const boundingRect = e.getBoundingClientRect();
|
|
return {
|
|
top: boundingRect.top * zoom,
|
|
bottom: boundingRect.bottom * zoom,
|
|
right: boundingRect.right * zoom,
|
|
left: boundingRect.left * zoom,
|
|
};
|
|
};
|
|
|
|
const targetBounds = this._target.targetElements.map(e => getZoomAccountedBoundingClientRect(e));
|
|
const { top, right, bottom, left } = targetBounds[0];
|
|
const width = right - left;
|
|
const height = bottom - top;
|
|
|
|
const targetRect: TargetRect = {
|
|
top, right, bottom, left, width, height,
|
|
center: {
|
|
x: left + (width / 2),
|
|
y: top + (height / 2)
|
|
}
|
|
};
|
|
|
|
// These calls adjust the position depending on spacing.
|
|
this.adjustHorizontalHoverPosition(targetRect);
|
|
this.adjustVerticalHoverPosition(targetRect);
|
|
// This call limits the maximum height of the hover.
|
|
this.adjustHoverMaxHeight(targetRect);
|
|
|
|
// Offset the hover position if there is a pointer so it aligns with the target element
|
|
this._hoverContainer.style.padding = '';
|
|
this._hoverContainer.style.margin = '';
|
|
if (this._hoverPointer) {
|
|
switch (this._hoverPosition) {
|
|
case HoverPosition.RIGHT:
|
|
targetRect.left += Constants.PointerSize;
|
|
targetRect.right += Constants.PointerSize;
|
|
this._hoverContainer.style.paddingLeft = `${Constants.PointerSize}px`;
|
|
this._hoverContainer.style.marginLeft = `${-Constants.PointerSize}px`;
|
|
break;
|
|
case HoverPosition.LEFT:
|
|
targetRect.left -= Constants.PointerSize;
|
|
targetRect.right -= Constants.PointerSize;
|
|
this._hoverContainer.style.paddingRight = `${Constants.PointerSize}px`;
|
|
this._hoverContainer.style.marginRight = `${-Constants.PointerSize}px`;
|
|
break;
|
|
case HoverPosition.BELOW:
|
|
targetRect.top += Constants.PointerSize;
|
|
targetRect.bottom += Constants.PointerSize;
|
|
this._hoverContainer.style.paddingTop = `${Constants.PointerSize}px`;
|
|
this._hoverContainer.style.marginTop = `${-Constants.PointerSize}px`;
|
|
break;
|
|
case HoverPosition.ABOVE:
|
|
targetRect.top -= Constants.PointerSize;
|
|
targetRect.bottom -= Constants.PointerSize;
|
|
this._hoverContainer.style.paddingBottom = `${Constants.PointerSize}px`;
|
|
this._hoverContainer.style.marginBottom = `${-Constants.PointerSize}px`;
|
|
break;
|
|
}
|
|
|
|
targetRect.center.x = targetRect.left + (width / 2);
|
|
targetRect.center.y = targetRect.top + (height / 2);
|
|
}
|
|
|
|
this.computeXCordinate(targetRect);
|
|
this.computeYCordinate(targetRect);
|
|
|
|
if (this._hoverPointer) {
|
|
// reset
|
|
this._hoverPointer.classList.remove('top');
|
|
this._hoverPointer.classList.remove('left');
|
|
this._hoverPointer.classList.remove('right');
|
|
this._hoverPointer.classList.remove('bottom');
|
|
|
|
this.setHoverPointerPosition(targetRect);
|
|
}
|
|
|
|
this._hover.onContentsChanged();
|
|
}
|
|
|
|
private computeXCordinate(target: TargetRect): void {
|
|
const hoverWidth = this._hover.containerDomNode.clientWidth + Constants.HoverBorderWidth;
|
|
|
|
if (this._target.x !== undefined) {
|
|
this._x = this._target.x;
|
|
}
|
|
|
|
else if (this._hoverPosition === HoverPosition.RIGHT) {
|
|
this._x = target.right;
|
|
}
|
|
|
|
else if (this._hoverPosition === HoverPosition.LEFT) {
|
|
this._x = target.left - hoverWidth;
|
|
}
|
|
|
|
else {
|
|
if (this._hoverPointer) {
|
|
this._x = target.center.x - (this._hover.containerDomNode.clientWidth / 2);
|
|
} else {
|
|
this._x = target.left;
|
|
}
|
|
|
|
// Hover is going beyond window towards right end
|
|
if (this._x + hoverWidth >= this._targetDocumentElement.clientWidth) {
|
|
this._hover.containerDomNode.classList.add('right-aligned');
|
|
this._x = Math.max(this._targetDocumentElement.clientWidth - hoverWidth - Constants.HoverWindowEdgeMargin, this._targetDocumentElement.clientLeft);
|
|
}
|
|
}
|
|
|
|
// Hover is going beyond window towards left end
|
|
if (this._x < this._targetDocumentElement.clientLeft) {
|
|
this._x = target.left + Constants.HoverWindowEdgeMargin;
|
|
}
|
|
|
|
}
|
|
|
|
private computeYCordinate(target: TargetRect): void {
|
|
if (this._target.y !== undefined) {
|
|
this._y = this._target.y;
|
|
}
|
|
|
|
else if (this._hoverPosition === HoverPosition.ABOVE) {
|
|
this._y = target.top;
|
|
}
|
|
|
|
else if (this._hoverPosition === HoverPosition.BELOW) {
|
|
this._y = target.bottom - 2;
|
|
}
|
|
|
|
else {
|
|
if (this._hoverPointer) {
|
|
this._y = target.center.y + (this._hover.containerDomNode.clientHeight / 2);
|
|
} else {
|
|
this._y = target.bottom;
|
|
}
|
|
}
|
|
|
|
// Hover on bottom is going beyond window
|
|
if (this._y > this._targetWindow.innerHeight) {
|
|
this._y = target.bottom;
|
|
}
|
|
}
|
|
|
|
private adjustHorizontalHoverPosition(target: TargetRect): void {
|
|
// Do not adjust horizontal hover position if x cordiante is provided
|
|
if (this._target.x !== undefined) {
|
|
return;
|
|
}
|
|
|
|
const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0);
|
|
|
|
// When force position is enabled, restrict max width
|
|
if (this._forcePosition) {
|
|
const padding = hoverPointerOffset + Constants.HoverBorderWidth;
|
|
if (this._hoverPosition === HoverPosition.RIGHT) {
|
|
this._hover.containerDomNode.style.maxWidth = `${this._targetDocumentElement.clientWidth - target.right - padding}px`;
|
|
} else if (this._hoverPosition === HoverPosition.LEFT) {
|
|
this._hover.containerDomNode.style.maxWidth = `${target.left - padding}px`;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Position hover on right to target
|
|
if (this._hoverPosition === HoverPosition.RIGHT) {
|
|
const roomOnRight = this._targetDocumentElement.clientWidth - target.right;
|
|
// Hover on the right is going beyond window.
|
|
if (roomOnRight < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
|
|
const roomOnLeft = target.left;
|
|
// There's enough room on the left, flip the hover position
|
|
if (roomOnLeft >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
|
|
this._hoverPosition = HoverPosition.LEFT;
|
|
}
|
|
// Hover on the left would go beyond window too
|
|
else {
|
|
this._hoverPosition = HoverPosition.BELOW;
|
|
}
|
|
}
|
|
}
|
|
// Position hover on left to target
|
|
else if (this._hoverPosition === HoverPosition.LEFT) {
|
|
|
|
const roomOnLeft = target.left;
|
|
// Hover on the left is going beyond window.
|
|
if (roomOnLeft < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
|
|
const roomOnRight = this._targetDocumentElement.clientWidth - target.right;
|
|
// There's enough room on the right, flip the hover position
|
|
if (roomOnRight >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
|
|
this._hoverPosition = HoverPosition.RIGHT;
|
|
}
|
|
// Hover on the right would go beyond window too
|
|
else {
|
|
this._hoverPosition = HoverPosition.BELOW;
|
|
}
|
|
}
|
|
// Hover on the left is going beyond window.
|
|
if (target.left - this._hover.containerDomNode.clientWidth - hoverPointerOffset <= this._targetDocumentElement.clientLeft) {
|
|
this._hoverPosition = HoverPosition.RIGHT;
|
|
}
|
|
}
|
|
}
|
|
|
|
private adjustVerticalHoverPosition(target: TargetRect): void {
|
|
// Do not adjust vertical hover position if the y coordinate is provided
|
|
// or the position is forced
|
|
if (this._target.y !== undefined || this._forcePosition) {
|
|
return;
|
|
}
|
|
|
|
const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0);
|
|
|
|
// Position hover on top of the target
|
|
if (this._hoverPosition === HoverPosition.ABOVE) {
|
|
// Hover on top is going beyond window
|
|
if (target.top - this._hover.containerDomNode.clientHeight - hoverPointerOffset < 0) {
|
|
this._hoverPosition = HoverPosition.BELOW;
|
|
}
|
|
}
|
|
|
|
// Position hover below the target
|
|
else if (this._hoverPosition === HoverPosition.BELOW) {
|
|
// Hover on bottom is going beyond window
|
|
if (target.bottom + this._hover.containerDomNode.clientHeight + hoverPointerOffset > this._targetWindow.innerHeight) {
|
|
this._hoverPosition = HoverPosition.ABOVE;
|
|
}
|
|
}
|
|
}
|
|
|
|
private adjustHoverMaxHeight(target: TargetRect): void {
|
|
let maxHeight = this._targetWindow.innerHeight / 2;
|
|
|
|
// When force position is enabled, restrict max height
|
|
if (this._forcePosition) {
|
|
const padding = (this._hoverPointer ? Constants.PointerSize : 0) + Constants.HoverBorderWidth;
|
|
if (this._hoverPosition === HoverPosition.ABOVE) {
|
|
maxHeight = Math.min(maxHeight, target.top - padding);
|
|
} else if (this._hoverPosition === HoverPosition.BELOW) {
|
|
maxHeight = Math.min(maxHeight, this._targetWindow.innerHeight - target.bottom - padding);
|
|
}
|
|
}
|
|
|
|
this._hover.containerDomNode.style.maxHeight = `${maxHeight}px`;
|
|
if (this._hover.contentsDomNode.clientHeight < this._hover.contentsDomNode.scrollHeight) {
|
|
// Add padding for a vertical scrollbar
|
|
const extraRightPadding = `${this._hover.scrollbar.options.verticalScrollbarSize}px`;
|
|
if (this._hover.contentsDomNode.style.paddingRight !== extraRightPadding) {
|
|
this._hover.contentsDomNode.style.paddingRight = extraRightPadding;
|
|
}
|
|
}
|
|
}
|
|
|
|
private setHoverPointerPosition(target: TargetRect): void {
|
|
if (!this._hoverPointer) {
|
|
return;
|
|
}
|
|
|
|
switch (this._hoverPosition) {
|
|
case HoverPosition.LEFT:
|
|
case HoverPosition.RIGHT: {
|
|
this._hoverPointer.classList.add(this._hoverPosition === HoverPosition.LEFT ? 'right' : 'left');
|
|
const hoverHeight = this._hover.containerDomNode.clientHeight;
|
|
|
|
// If hover is taller than target, then show the pointer at the center of target
|
|
if (hoverHeight > target.height) {
|
|
this._hoverPointer.style.top = `${target.center.y - (this._y - hoverHeight) - Constants.PointerSize}px`;
|
|
}
|
|
|
|
// Otherwise show the pointer at the center of hover
|
|
else {
|
|
this._hoverPointer.style.top = `${Math.round((hoverHeight / 2)) - Constants.PointerSize}px`;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case HoverPosition.ABOVE:
|
|
case HoverPosition.BELOW: {
|
|
this._hoverPointer.classList.add(this._hoverPosition === HoverPosition.ABOVE ? 'bottom' : 'top');
|
|
const hoverWidth = this._hover.containerDomNode.clientWidth;
|
|
|
|
// Position pointer at the center of the hover
|
|
let pointerLeftPosition = Math.round((hoverWidth / 2)) - Constants.PointerSize;
|
|
|
|
// If pointer goes beyond target then position it at the center of the target
|
|
const pointerX = this._x + pointerLeftPosition;
|
|
if (pointerX < target.left || pointerX > target.right) {
|
|
pointerLeftPosition = target.center.x - this._x - Constants.PointerSize;
|
|
}
|
|
|
|
this._hoverPointer.style.left = `${pointerLeftPosition}px`;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public focus() {
|
|
this._hover.containerDomNode.focus();
|
|
}
|
|
|
|
public hide(): void {
|
|
this.dispose();
|
|
}
|
|
|
|
public override dispose(): void {
|
|
if (!this._isDisposed) {
|
|
this._onDispose.fire();
|
|
this._hoverContainer.remove();
|
|
this._messageListeners.dispose();
|
|
this._target.dispose();
|
|
super.dispose();
|
|
}
|
|
this._isDisposed = true;
|
|
}
|
|
}
|
|
|
|
class CompositeMouseTracker extends Widget {
|
|
private _isMouseIn: boolean = true;
|
|
private _mouseTimeout: number | undefined;
|
|
|
|
private readonly _onMouseOut = this._register(new Emitter<void>());
|
|
get onMouseOut(): Event<void> { return this._onMouseOut.event; }
|
|
|
|
get isMouseIn(): boolean { return this._isMouseIn; }
|
|
|
|
constructor(
|
|
private _elements: HTMLElement[]
|
|
) {
|
|
super();
|
|
this._elements.forEach(n => this.onmouseover(n, () => this._onTargetMouseOver(n)));
|
|
this._elements.forEach(n => this.onmouseleave(n, () => this._onTargetMouseLeave(n)));
|
|
}
|
|
|
|
private _onTargetMouseOver(target: HTMLElement): void {
|
|
this._isMouseIn = true;
|
|
this._clearEvaluateMouseStateTimeout(target);
|
|
}
|
|
|
|
private _onTargetMouseLeave(target: HTMLElement): void {
|
|
this._isMouseIn = false;
|
|
this._evaluateMouseState(target);
|
|
}
|
|
|
|
private _evaluateMouseState(target: HTMLElement): void {
|
|
this._clearEvaluateMouseStateTimeout(target);
|
|
// Evaluate whether the mouse is still outside asynchronously such that other mouse targets
|
|
// have the opportunity to first their mouse in event.
|
|
this._mouseTimeout = dom.getWindow(target).setTimeout(() => this._fireIfMouseOutside(), 0);
|
|
}
|
|
|
|
private _clearEvaluateMouseStateTimeout(target: HTMLElement): void {
|
|
if (this._mouseTimeout) {
|
|
dom.getWindow(target).clearTimeout(this._mouseTimeout);
|
|
this._mouseTimeout = undefined;
|
|
}
|
|
}
|
|
|
|
private _fireIfMouseOutside(): void {
|
|
if (!this._isMouseIn) {
|
|
this._onMouseOut.fire();
|
|
}
|
|
}
|
|
}
|
|
|
|
class ElementHoverTarget implements IHoverTarget {
|
|
readonly targetElements: readonly HTMLElement[];
|
|
|
|
constructor(
|
|
private _element: HTMLElement
|
|
) {
|
|
this.targetElements = [this._element];
|
|
}
|
|
|
|
dispose(): void {
|
|
}
|
|
}
|