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