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