Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/hover/browser/hoverWidget.ts
5221 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './hover.css';
7
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
8
import { Event, Emitter } from '../../../base/common/event.js';
9
import * as dom from '../../../base/browser/dom.js';
10
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
11
import { KeyCode } from '../../../base/common/keyCodes.js';
12
import { IConfigurationService } from '../../configuration/common/configuration.js';
13
import { HoverAction, HoverPosition, HoverWidget as BaseHoverWidget, getHoverAccessibleViewHint } from '../../../base/browser/ui/hover/hoverWidget.js';
14
import { Widget } from '../../../base/browser/ui/widget.js';
15
import { AnchorPosition } from '../../../base/browser/ui/contextview/contextview.js';
16
import { IMarkdownRendererService } from '../../markdown/browser/markdownRenderer.js';
17
import { isMarkdownString } from '../../../base/common/htmlContent.js';
18
import { localize } from '../../../nls.js';
19
import { isMacintosh } from '../../../base/common/platform.js';
20
import { IAccessibilityService } from '../../accessibility/common/accessibility.js';
21
import { status } from '../../../base/browser/ui/aria/aria.js';
22
import { HoverStyle, type IHoverOptions, type IHoverTarget, type IHoverWidget } from '../../../base/browser/ui/hover/hover.js';
23
import { TimeoutTimer } from '../../../base/common/async.js';
24
import { isNumber } from '../../../base/common/types.js';
25
26
const $ = dom.$;
27
type TargetRect = {
28
left: number;
29
right: number;
30
top: number;
31
bottom: number;
32
width: number;
33
height: number;
34
center: { x: number; y: number };
35
};
36
37
const enum Constants {
38
PointerSize = 3,
39
HoverBorderWidth = 2,
40
HoverWindowEdgeMargin = 2,
41
}
42
43
export class HoverWidget extends Widget implements IHoverWidget {
44
private readonly _messageListeners = new DisposableStore();
45
private readonly _lockMouseTracker: CompositeMouseTracker;
46
47
private readonly _hover: BaseHoverWidget;
48
private readonly _hoverPointer: HTMLElement | undefined;
49
private readonly _hoverContainer: HTMLElement;
50
private readonly _target: IHoverTarget;
51
private readonly _linkHandler: ((url: string) => void) | undefined;
52
53
private _isDisposed: boolean = false;
54
private _hoverPosition: HoverPosition;
55
private _forcePosition: boolean = false;
56
private _x: number = 0;
57
private _y: number = 0;
58
private _isLocked: boolean = false;
59
private _enableFocusTraps: boolean = false;
60
private _addedFocusTrap: boolean = false;
61
private _maxHeightRatioRelativeToWindow: number = 0.5;
62
63
private get _targetWindow(): Window {
64
return dom.getWindow(this._target.targetElements[0]);
65
}
66
private get _targetDocumentElement(): HTMLElement {
67
return dom.getWindow(this._target.targetElements[0]).document.documentElement;
68
}
69
70
get isDisposed(): boolean { return this._isDisposed; }
71
get isMouseIn(): boolean { return this._lockMouseTracker.isMouseIn; }
72
get domNode(): HTMLElement { return this._hover.containerDomNode; }
73
74
private readonly _onDispose = this._register(new Emitter<void>());
75
get onDispose(): Event<void> { return this._onDispose.event; }
76
private readonly _onRequestLayout = this._register(new Emitter<void>());
77
get onRequestLayout(): Event<void> { return this._onRequestLayout.event; }
78
79
get anchor(): AnchorPosition { return this._hoverPosition === HoverPosition.BELOW ? AnchorPosition.BELOW : AnchorPosition.ABOVE; }
80
get x(): number { return this._x; }
81
get y(): number { return this._y; }
82
83
/**
84
* Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not
85
* hide and can be hovered regardless of whether the `hideOnHover` hover option is set.
86
*/
87
get isLocked(): boolean { return this._isLocked; }
88
set isLocked(value: boolean) {
89
if (this._isLocked === value) {
90
return;
91
}
92
this._isLocked = value;
93
this._hoverContainer.classList.toggle('locked', this._isLocked);
94
}
95
96
/**
97
* Adds an element to be tracked by this hover's mouse tracker. Mouse events on
98
* this element will be considered as being "inside" the hover, preventing it
99
* from closing. This is used for nested hovers where the child hover's container
100
* should be treated as part of the parent hover.
101
*/
102
addMouseTrackingElement(element: HTMLElement): IDisposable {
103
return this._lockMouseTracker.addElement(element);
104
}
105
106
constructor(
107
options: IHoverOptions,
108
@IKeybindingService private readonly _keybindingService: IKeybindingService,
109
@IConfigurationService private readonly _configurationService: IConfigurationService,
110
@IMarkdownRendererService private readonly _markdownRenderer: IMarkdownRendererService,
111
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
112
) {
113
super();
114
115
this._linkHandler = options.linkHandler;
116
117
this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target);
118
119
if (options.style) {
120
switch (options.style) {
121
case HoverStyle.Pointer: {
122
options.appearance ??= {};
123
options.appearance.compact ??= true;
124
options.appearance.showPointer ??= true;
125
break;
126
}
127
case HoverStyle.Mouse: {
128
options.appearance ??= {};
129
options.appearance.compact ??= true;
130
break;
131
}
132
}
133
}
134
135
this._hoverPointer = options.appearance?.showPointer ? $('div.workbench-hover-pointer') : undefined;
136
this._hover = this._register(new BaseHoverWidget(!options.appearance?.skipFadeInAnimation));
137
this._hover.containerDomNode.classList.add('workbench-hover');
138
if (options.appearance?.compact) {
139
this._hover.containerDomNode.classList.add('workbench-hover', 'compact');
140
}
141
if (options.additionalClasses) {
142
this._hover.containerDomNode.classList.add(...options.additionalClasses);
143
}
144
if (options.position?.forcePosition) {
145
this._forcePosition = true;
146
}
147
if (options.trapFocus) {
148
this._enableFocusTraps = true;
149
}
150
151
const maxHeightRatio = options.appearance?.maxHeightRatio;
152
if (maxHeightRatio !== undefined && maxHeightRatio > 0 && maxHeightRatio <= 1) {
153
this._maxHeightRatioRelativeToWindow = maxHeightRatio;
154
}
155
156
// Default to position above when the position is unspecified or a mouse event
157
this._hoverPosition = options.position?.hoverPosition === undefined
158
? HoverPosition.ABOVE
159
: isNumber(options.position.hoverPosition)
160
? options.position.hoverPosition
161
: HoverPosition.BELOW;
162
163
// Don't allow mousedown out of the widget, otherwise preventDefault will call and text will
164
// not be selected.
165
this.onmousedown(this._hover.containerDomNode, e => e.stopPropagation());
166
167
// Hide hover on escape
168
this.onkeydown(this._hover.containerDomNode, e => {
169
if (e.equals(KeyCode.Escape)) {
170
this.dispose();
171
}
172
});
173
174
// Hide when the window loses focus
175
this._register(dom.addDisposableListener(this._targetWindow, 'blur', () => this.dispose()));
176
177
const rowElement = $('div.hover-row.markdown-hover');
178
const contentsElement = $('div.hover-contents');
179
if (typeof options.content === 'string') {
180
contentsElement.textContent = options.content;
181
contentsElement.style.whiteSpace = 'pre-wrap';
182
183
} else if (dom.isHTMLElement(options.content)) {
184
contentsElement.appendChild(options.content);
185
contentsElement.classList.add('html-hover-contents');
186
187
} else {
188
const markdown = options.content;
189
190
const { element } = this._register(this._markdownRenderer.render(markdown, {
191
actionHandler: this._linkHandler,
192
asyncRenderCallback: () => {
193
contentsElement.classList.add('code-hover-contents');
194
this.layout();
195
// This changes the dimensions of the hover so trigger a layout
196
this._onRequestLayout.fire();
197
}
198
}));
199
contentsElement.appendChild(element);
200
}
201
rowElement.appendChild(contentsElement);
202
this._hover.contentsDomNode.appendChild(rowElement);
203
204
if (options.actions && options.actions.length > 0) {
205
const statusBarElement = $('div.hover-row.status-bar');
206
const actionsElement = $('div.actions');
207
options.actions.forEach(action => {
208
const keybinding = this._keybindingService.lookupKeybinding(action.commandId);
209
const keybindingLabel = keybinding ? keybinding.getLabel() : null;
210
this._register(HoverAction.render(actionsElement, {
211
label: action.label,
212
commandId: action.commandId,
213
run: e => {
214
action.run(e);
215
this.dispose();
216
},
217
iconClass: action.iconClass
218
}, keybindingLabel));
219
});
220
statusBarElement.appendChild(actionsElement);
221
this._hover.containerDomNode.appendChild(statusBarElement);
222
}
223
224
this._hoverContainer = $('div.workbench-hover-container');
225
if (this._hoverPointer) {
226
this._hoverContainer.appendChild(this._hoverPointer);
227
}
228
this._hoverContainer.appendChild(this._hover.containerDomNode);
229
230
// Determine whether to hide on hover
231
let hideOnHover: boolean;
232
if (options.actions && options.actions.length > 0) {
233
// If there are actions, require hover so they can be accessed
234
hideOnHover = false;
235
} else {
236
if (options.persistence?.hideOnHover === undefined) {
237
// When unset, will default to true when it's a string or when it's markdown that
238
// appears to have a link using a naive check for '](' and '</a>'
239
hideOnHover = typeof options.content === 'string' ||
240
isMarkdownString(options.content) && !options.content.value.includes('](') && !options.content.value.includes('</a>');
241
} else {
242
// It's set explicitly
243
hideOnHover = options.persistence.hideOnHover;
244
}
245
}
246
247
// Show the hover hint if needed
248
if (options.appearance?.showHoverHint) {
249
const statusBarElement = $('div.hover-row.status-bar');
250
const infoElement = $('div.info');
251
infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt');
252
statusBarElement.appendChild(infoElement);
253
this._hover.containerDomNode.appendChild(statusBarElement);
254
}
255
256
const mouseTrackerTargets = [...this._target.targetElements];
257
if (!hideOnHover) {
258
mouseTrackerTargets.push(this._hoverContainer);
259
}
260
const mouseTracker = this._register(new CompositeMouseTracker(mouseTrackerTargets));
261
this._register(mouseTracker.onMouseOut(() => {
262
if (!this._isLocked) {
263
this.dispose();
264
}
265
}));
266
267
// Setup another mouse tracker when hideOnHover is set in order to track the hover as well
268
// when it is locked. This ensures the hover will hide on mouseout after alt has been
269
// released to unlock the element.
270
if (hideOnHover) {
271
const mouseTracker2Targets = [...this._target.targetElements, this._hoverContainer];
272
this._lockMouseTracker = this._register(new CompositeMouseTracker(mouseTracker2Targets));
273
this._register(this._lockMouseTracker.onMouseOut(() => {
274
if (!this._isLocked) {
275
this.dispose();
276
}
277
}));
278
} else {
279
this._lockMouseTracker = mouseTracker;
280
}
281
}
282
283
private addFocusTrap() {
284
if (!this._enableFocusTraps || this._addedFocusTrap) {
285
return;
286
}
287
this._addedFocusTrap = true;
288
289
// Add a hover tab loop if the hover has at least one element with a valid tabIndex
290
const firstContainerFocusElement = this._hover.containerDomNode;
291
const lastContainerFocusElement = this.findLastFocusableChild(this._hover.containerDomNode);
292
if (lastContainerFocusElement) {
293
const beforeContainerFocusElement = dom.prepend(this._hoverContainer, $('div'));
294
const afterContainerFocusElement = dom.append(this._hoverContainer, $('div'));
295
beforeContainerFocusElement.tabIndex = 0;
296
afterContainerFocusElement.tabIndex = 0;
297
this._register(dom.addDisposableListener(afterContainerFocusElement, 'focus', (e) => {
298
firstContainerFocusElement.focus();
299
e.preventDefault();
300
}));
301
this._register(dom.addDisposableListener(beforeContainerFocusElement, 'focus', (e) => {
302
lastContainerFocusElement.focus();
303
e.preventDefault();
304
}));
305
}
306
}
307
308
private findLastFocusableChild(root: Node): HTMLElement | undefined {
309
if (root.hasChildNodes()) {
310
for (let i = 0; i < root.childNodes.length; i++) {
311
const node = root.childNodes.item(root.childNodes.length - i - 1);
312
if (node.nodeType === node.ELEMENT_NODE) {
313
const parsedNode = node as HTMLElement;
314
if (typeof parsedNode.tabIndex === 'number' && parsedNode.tabIndex >= 0) {
315
return parsedNode;
316
}
317
}
318
const recursivelyFoundElement = this.findLastFocusableChild(node);
319
if (recursivelyFoundElement) {
320
return recursivelyFoundElement;
321
}
322
}
323
}
324
return undefined;
325
}
326
327
public render(container: HTMLElement): void {
328
container.appendChild(this._hoverContainer);
329
const hoverFocused = this._hoverContainer.contains(this._hoverContainer.ownerDocument.activeElement);
330
const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint(this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(), this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel());
331
if (accessibleViewHint) {
332
333
status(accessibleViewHint);
334
}
335
this.layout();
336
this.addFocusTrap();
337
}
338
339
public layout() {
340
this._hover.containerDomNode.classList.remove('right-aligned');
341
this._hover.contentsDomNode.style.maxHeight = '';
342
343
const getZoomAccountedBoundingClientRect = (e: HTMLElement) => {
344
const zoom = dom.getDomNodeZoomLevel(e);
345
346
const boundingRect = e.getBoundingClientRect();
347
return {
348
top: boundingRect.top * zoom,
349
bottom: boundingRect.bottom * zoom,
350
right: boundingRect.right * zoom,
351
left: boundingRect.left * zoom,
352
};
353
};
354
355
const targetBounds = this._target.targetElements.map(e => getZoomAccountedBoundingClientRect(e));
356
const { top, right, bottom, left } = targetBounds[0];
357
const width = right - left;
358
const height = bottom - top;
359
360
const targetRect: TargetRect = {
361
top, right, bottom, left, width, height,
362
center: {
363
x: left + (width / 2),
364
y: top + (height / 2)
365
}
366
};
367
368
// These calls adjust the position depending on spacing.
369
this.adjustHorizontalHoverPosition(targetRect);
370
this.adjustVerticalHoverPosition(targetRect);
371
// This call limits the maximum height of the hover.
372
this.adjustHoverMaxHeight(targetRect);
373
374
// Offset the hover position if there is a pointer so it aligns with the target element
375
this._hoverContainer.style.padding = '';
376
this._hoverContainer.style.margin = '';
377
if (this._hoverPointer) {
378
switch (this._hoverPosition) {
379
case HoverPosition.RIGHT:
380
targetRect.left += Constants.PointerSize;
381
targetRect.right += Constants.PointerSize;
382
this._hoverContainer.style.paddingLeft = `${Constants.PointerSize}px`;
383
this._hoverContainer.style.marginLeft = `${-Constants.PointerSize}px`;
384
break;
385
case HoverPosition.LEFT:
386
targetRect.left -= Constants.PointerSize;
387
targetRect.right -= Constants.PointerSize;
388
this._hoverContainer.style.paddingRight = `${Constants.PointerSize}px`;
389
this._hoverContainer.style.marginRight = `${-Constants.PointerSize}px`;
390
break;
391
case HoverPosition.BELOW:
392
targetRect.top += Constants.PointerSize;
393
targetRect.bottom += Constants.PointerSize;
394
this._hoverContainer.style.paddingTop = `${Constants.PointerSize}px`;
395
this._hoverContainer.style.marginTop = `${-Constants.PointerSize}px`;
396
break;
397
case HoverPosition.ABOVE:
398
targetRect.top -= Constants.PointerSize;
399
targetRect.bottom -= Constants.PointerSize;
400
this._hoverContainer.style.paddingBottom = `${Constants.PointerSize}px`;
401
this._hoverContainer.style.marginBottom = `${-Constants.PointerSize}px`;
402
break;
403
}
404
405
targetRect.center.x = targetRect.left + (width / 2);
406
targetRect.center.y = targetRect.top + (height / 2);
407
}
408
409
this.computeXCordinate(targetRect);
410
this.computeYCordinate(targetRect);
411
412
if (this._hoverPointer) {
413
// reset
414
this._hoverPointer.classList.remove('top');
415
this._hoverPointer.classList.remove('left');
416
this._hoverPointer.classList.remove('right');
417
this._hoverPointer.classList.remove('bottom');
418
419
this.setHoverPointerPosition(targetRect);
420
}
421
this._hover.onContentsChanged();
422
}
423
424
private computeXCordinate(target: TargetRect): void {
425
const hoverWidth = this._hover.containerDomNode.clientWidth + Constants.HoverBorderWidth;
426
427
if (this._target.x !== undefined) {
428
this._x = this._target.x;
429
}
430
431
else if (this._hoverPosition === HoverPosition.RIGHT) {
432
this._x = target.right;
433
}
434
435
else if (this._hoverPosition === HoverPosition.LEFT) {
436
this._x = target.left - hoverWidth;
437
}
438
439
else {
440
if (this._hoverPointer) {
441
this._x = target.center.x - (this._hover.containerDomNode.clientWidth / 2);
442
} else {
443
this._x = target.left;
444
}
445
446
// Hover is going beyond window towards right end
447
if (this._x + hoverWidth >= this._targetDocumentElement.clientWidth) {
448
this._hover.containerDomNode.classList.add('right-aligned');
449
this._x = Math.max(this._targetDocumentElement.clientWidth - hoverWidth - Constants.HoverWindowEdgeMargin, this._targetDocumentElement.clientLeft);
450
}
451
}
452
453
// Hover is going beyond window towards left end
454
if (this._x < this._targetDocumentElement.clientLeft) {
455
this._x = target.left + Constants.HoverWindowEdgeMargin;
456
}
457
458
}
459
460
private computeYCordinate(target: TargetRect): void {
461
if (this._target.y !== undefined) {
462
this._y = this._target.y;
463
}
464
465
else if (this._hoverPosition === HoverPosition.ABOVE) {
466
this._y = target.top;
467
}
468
469
else if (this._hoverPosition === HoverPosition.BELOW) {
470
this._y = target.bottom - 2;
471
}
472
473
else {
474
if (this._hoverPointer) {
475
this._y = target.center.y + (this._hover.containerDomNode.clientHeight / 2);
476
} else {
477
this._y = target.bottom;
478
}
479
}
480
481
// Hover on bottom is going beyond window
482
if (this._y > this._targetWindow.innerHeight) {
483
this._y = target.bottom;
484
}
485
}
486
487
private adjustHorizontalHoverPosition(target: TargetRect): void {
488
// Do not adjust horizontal hover position if x cordiante is provided
489
if (this._target.x !== undefined) {
490
return;
491
}
492
493
const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0);
494
495
// When force position is enabled, restrict max width
496
if (this._forcePosition) {
497
const padding = hoverPointerOffset + Constants.HoverBorderWidth;
498
if (this._hoverPosition === HoverPosition.RIGHT) {
499
this._hover.containerDomNode.style.maxWidth = `${this._targetDocumentElement.clientWidth - target.right - padding}px`;
500
} else if (this._hoverPosition === HoverPosition.LEFT) {
501
this._hover.containerDomNode.style.maxWidth = `${target.left - padding}px`;
502
}
503
return;
504
}
505
506
// Position hover on right to target
507
if (this._hoverPosition === HoverPosition.RIGHT) {
508
const roomOnRight = this._targetDocumentElement.clientWidth - target.right;
509
// Hover on the right is going beyond window.
510
if (roomOnRight < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
511
const roomOnLeft = target.left;
512
// There's enough room on the left, flip the hover position
513
if (roomOnLeft >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
514
this._hoverPosition = HoverPosition.LEFT;
515
}
516
// Hover on the left would go beyond window too
517
else {
518
this._hoverPosition = HoverPosition.BELOW;
519
}
520
}
521
}
522
// Position hover on left to target
523
else if (this._hoverPosition === HoverPosition.LEFT) {
524
525
const roomOnLeft = target.left;
526
// Hover on the left is going beyond window.
527
if (roomOnLeft < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
528
const roomOnRight = this._targetDocumentElement.clientWidth - target.right;
529
// There's enough room on the right, flip the hover position
530
if (roomOnRight >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
531
this._hoverPosition = HoverPosition.RIGHT;
532
}
533
// Hover on the right would go beyond window too
534
else {
535
this._hoverPosition = HoverPosition.BELOW;
536
}
537
}
538
// Hover on the left is going beyond window.
539
if (target.left - this._hover.containerDomNode.clientWidth - hoverPointerOffset <= this._targetDocumentElement.clientLeft) {
540
this._hoverPosition = HoverPosition.RIGHT;
541
}
542
}
543
}
544
545
private adjustVerticalHoverPosition(target: TargetRect): void {
546
// Do not adjust vertical hover position if the y coordinate is provided
547
// or the position is forced
548
if (this._target.y !== undefined || this._forcePosition) {
549
return;
550
}
551
552
const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0);
553
554
// Position hover on top of the target
555
if (this._hoverPosition === HoverPosition.ABOVE) {
556
// Hover on top is going beyond window
557
if (target.top - this._hover.containerDomNode.clientHeight - hoverPointerOffset < 0) {
558
this._hoverPosition = HoverPosition.BELOW;
559
}
560
}
561
562
// Position hover below the target
563
else if (this._hoverPosition === HoverPosition.BELOW) {
564
// Hover on bottom is going beyond window
565
if (target.bottom + this._hover.containerDomNode.offsetHeight + hoverPointerOffset > this._targetWindow.innerHeight) {
566
this._hoverPosition = HoverPosition.ABOVE;
567
}
568
}
569
}
570
571
private adjustHoverMaxHeight(target: TargetRect): void {
572
let maxHeight = this._targetWindow.innerHeight * this._maxHeightRatioRelativeToWindow;
573
574
// When force position is enabled, restrict max height
575
if (this._forcePosition) {
576
const padding = (this._hoverPointer ? Constants.PointerSize : 0) + Constants.HoverBorderWidth;
577
if (this._hoverPosition === HoverPosition.ABOVE) {
578
maxHeight = Math.min(maxHeight, target.top - padding);
579
} else if (this._hoverPosition === HoverPosition.BELOW) {
580
maxHeight = Math.min(maxHeight, this._targetWindow.innerHeight - target.bottom - padding);
581
}
582
}
583
584
this._hover.containerDomNode.style.maxHeight = `${maxHeight}px`;
585
if (this._hover.contentsDomNode.clientHeight < this._hover.contentsDomNode.scrollHeight) {
586
// Add padding for a vertical scrollbar
587
const extraRightPadding = `${this._hover.scrollbar.options.verticalScrollbarSize}px`;
588
if (this._hover.contentsDomNode.style.paddingRight !== extraRightPadding) {
589
this._hover.contentsDomNode.style.paddingRight = extraRightPadding;
590
}
591
}
592
}
593
594
private setHoverPointerPosition(target: TargetRect): void {
595
if (!this._hoverPointer) {
596
return;
597
}
598
599
switch (this._hoverPosition) {
600
case HoverPosition.LEFT:
601
case HoverPosition.RIGHT: {
602
this._hoverPointer.classList.add(this._hoverPosition === HoverPosition.LEFT ? 'right' : 'left');
603
const hoverHeight = this._hover.containerDomNode.clientHeight;
604
605
// If hover is taller than target, then show the pointer at the center of target
606
if (hoverHeight > target.height) {
607
this._hoverPointer.style.top = `${target.center.y - (this._y - hoverHeight) - Constants.PointerSize}px`;
608
}
609
610
// Otherwise show the pointer at the center of hover
611
else {
612
this._hoverPointer.style.top = `${Math.round((hoverHeight / 2)) - Constants.PointerSize}px`;
613
}
614
615
break;
616
}
617
case HoverPosition.ABOVE:
618
case HoverPosition.BELOW: {
619
this._hoverPointer.classList.add(this._hoverPosition === HoverPosition.ABOVE ? 'bottom' : 'top');
620
const hoverWidth = this._hover.containerDomNode.clientWidth;
621
622
// Position pointer at the center of the hover
623
let pointerLeftPosition = Math.round((hoverWidth / 2)) - Constants.PointerSize;
624
625
// If pointer goes beyond target then position it at the center of the target
626
const pointerX = this._x + pointerLeftPosition;
627
if (pointerX < target.left || pointerX > target.right) {
628
pointerLeftPosition = target.center.x - this._x - Constants.PointerSize;
629
}
630
631
this._hoverPointer.style.left = `${pointerLeftPosition}px`;
632
break;
633
}
634
}
635
}
636
637
public focus() {
638
this._hover.containerDomNode.focus();
639
}
640
641
public hide(): void {
642
this.dispose();
643
}
644
645
public override dispose(): void {
646
if (!this._isDisposed) {
647
this._onDispose.fire();
648
this._target.dispose?.();
649
this._hoverContainer.remove();
650
this._messageListeners.dispose();
651
super.dispose();
652
}
653
this._isDisposed = true;
654
}
655
}
656
657
class CompositeMouseTracker extends Widget {
658
private _isMouseIn: boolean = true;
659
private readonly _mouseTimer: MutableDisposable<TimeoutTimer> = this._register(new MutableDisposable());
660
661
private readonly _onMouseOut = this._register(new Emitter<void>());
662
get onMouseOut(): Event<void> { return this._onMouseOut.event; }
663
664
get isMouseIn(): boolean { return this._isMouseIn; }
665
666
/**
667
* @param _elements The target elements to track mouse in/out events on.
668
* @param _eventDebounceDelay The delay in ms to debounce the event firing. This is used to
669
* allow a short period for the mouse to move into the hover or a nearby target element. For
670
* example hovering a scroll bar will not hide the hover immediately.
671
*/
672
constructor(
673
private _elements: HTMLElement[],
674
private _eventDebounceDelay: number = 200
675
) {
676
super();
677
678
for (const element of this._elements) {
679
this.onmouseover(element, () => this._onTargetMouseOver());
680
this.onmouseleave(element, () => this._onTargetMouseLeave());
681
}
682
}
683
684
private _onTargetMouseOver(): void {
685
this._isMouseIn = true;
686
this._mouseTimer.clear();
687
}
688
689
private _onTargetMouseLeave(): void {
690
this._isMouseIn = false;
691
// Evaluate whether the mouse is still outside asynchronously such that other mouse targets
692
// have the opportunity to first their mouse in event.
693
this._mouseTimer.value = new TimeoutTimer(() => this._fireIfMouseOutside(), this._eventDebounceDelay);
694
}
695
696
private _fireIfMouseOutside(): void {
697
if (!this._isMouseIn) {
698
this._onMouseOut.fire();
699
}
700
}
701
702
/**
703
* Adds an element to be tracked by this mouse tracker. Mouse events on this
704
* element will be considered as being "inside" the tracked area.
705
*/
706
addElement(element: HTMLElement): IDisposable {
707
if (this._elements.includes(element)) {
708
return Disposable.None;
709
}
710
this._elements.push(element);
711
const store = new DisposableStore();
712
store.add(dom.addDisposableListener(element, dom.EventType.MOUSE_OVER, () => this._onTargetMouseOver()));
713
store.add(dom.addDisposableListener(element, dom.EventType.MOUSE_LEAVE, () => this._onTargetMouseLeave()));
714
store.add(toDisposable(() => {
715
const index = this._elements.indexOf(element);
716
if (index >= 0) {
717
this._elements.splice(index, 1);
718
}
719
}));
720
return store;
721
}
722
}
723
724
class ElementHoverTarget implements IHoverTarget {
725
readonly targetElements: readonly HTMLElement[];
726
727
constructor(
728
private _element: HTMLElement
729
) {
730
this.targetElements = [this._element];
731
}
732
733
dispose(): void {
734
}
735
}
736
737