-
Notifications
You must be signed in to change notification settings - Fork 30k
/
Copy pathhoverWidget.ts
271 lines (232 loc) · 9.64 KB
/
hoverWidget.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle';
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
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 { IHoverTarget, IHoverOptions } from 'vs/workbench/services/hover/browser/hover';
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 { HoverWidget as BaseHoverWidget, renderHoverAction } 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 { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { MarkdownString } from 'vs/base/common/htmlContent';
const $ = dom.$;
export class HoverWidget extends Widget {
private readonly _messageListeners = new DisposableStore();
private readonly _mouseTracker: CompositeMouseTracker;
private readonly _hover: BaseHoverWidget;
private readonly _target: IHoverTarget;
private readonly _linkHandler: (url: string) => any;
private _isDisposed: boolean = false;
private _anchor: AnchorPosition;
private _x: number = 0;
private _y: number = 0;
get isDisposed(): boolean { return this._isDisposed; }
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._anchor; }
get x(): number { return this._x; }
get y(): number { return this._y; }
constructor(
options: IHoverOptions,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IOpenerService private readonly _openerService: IOpenerService,
@IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService,
) {
super();
this._linkHandler = options.linkHandler || this._openerService.open;
this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target);
this._hover = this._register(new BaseHoverWidget());
this._hover.containerDomNode.classList.add('workbench-hover', 'fadeIn');
if (options.additionalClasses) {
this._hover.containerDomNode.classList.add(...options.additionalClasses);
}
this._anchor = options.anchorPosition ?? AnchorPosition.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();
}
});
const rowElement = $('div.hover-row.markdown-hover');
const contentsElement = $('div.hover-contents');
const markdown = typeof options.text === 'string' ? new MarkdownString().appendText(options.text) : options.text;
const markdownElement = renderMarkdown(markdown, {
actionHandler: {
callback: (content) => this._linkHandler(content),
disposeables: this._messageListeners
},
codeBlockRenderer: async (_, value) => {
const fontFamily = this._configurationService.getValue<IEditorOptions>('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily;
return `<span style="font-family: ${fontFamily}; white-space: nowrap">${value.replace(/\n/g, '<br>')}</span>`;
},
codeBlockRenderCallback: () => {
contentsElement.classList.add('code-hover-contents');
// This changes the dimensions of the hover so trigger a layout
this._onRequestLayout.fire();
}
});
contentsElement.appendChild(markdownElement);
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;
renderHoverAction(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);
}
const mouseTrackerTargets = [...this._target.targetElements];
let hideOnHover: boolean;
if (options.hideOnHover === undefined) {
if (options.actions && options.actions.length > 0) {
// If there are actions, require hover so they can be accessed
hideOnHover = false;
} else {
// Defaults to true when string, false when markdown as it may contain links
hideOnHover = typeof options.text === 'string';
}
} else {
// It's set explicitly
hideOnHover = options.hideOnHover;
}
if (!hideOnHover) {
mouseTrackerTargets.push(this._hover.containerDomNode);
}
this._mouseTracker = new CompositeMouseTracker(mouseTrackerTargets);
this._register(this._mouseTracker.onMouseOut(() => this.dispose()));
this._register(this._mouseTracker);
}
public render(container?: HTMLElement): void {
if (this._hover.containerDomNode.parentElement !== container) {
container?.appendChild(this._hover.containerDomNode);
}
this.layout();
}
public layout() {
this._hover.containerDomNode.classList.remove('right-aligned');
this._hover.contentsDomNode.style.maxHeight = '';
const targetBounds = this._target.targetElements.map(e => e.getBoundingClientRect());
// Get horizontal alignment and position
let targetLeft = this._target.x !== undefined ? this._target.x : Math.min(...targetBounds.map(e => e.left));
if (targetLeft + this._hover.containerDomNode.clientWidth >= document.documentElement.clientWidth) {
this._x = document.documentElement.clientWidth - this._workbenchLayoutService.getWindowBorderWidth() - 1;
this._hover.containerDomNode.classList.add('right-aligned');
} else {
this._x = targetLeft;
}
// Get vertical alignment and position
if (this._anchor === AnchorPosition.ABOVE) {
const targetTop = Math.min(...targetBounds.map(e => e.top));
if (targetTop - this._hover.containerDomNode.clientHeight < 0) {
const targetBottom = Math.max(...targetBounds.map(e => e.bottom));
this._anchor = AnchorPosition.BELOW;
this._y = targetBottom - 2;
} else {
this._y = targetTop;
}
} else {
console.log('below');
const targetBottom = Math.max(...targetBounds.map(e => e.bottom));
if (targetBottom + this._hover.containerDomNode.clientHeight > window.innerHeight) {
console.log(targetBottom, this._hover.containerDomNode.clientHeight, window.innerHeight);
const targetTop = Math.min(...targetBounds.map(e => e.top));
this._anchor = AnchorPosition.ABOVE;
this._y = targetTop;
} else {
this._y = targetBottom - 2;
}
}
this._hover.onContentsChanged();
}
public focus() {
this._hover.containerDomNode.focus();
}
public hide(): void {
this.dispose();
}
public dispose(): void {
if (!this._isDisposed) {
this._onDispose.fire();
this._hover.containerDomNode.parentElement?.removeChild(this.domNode);
this._messageListeners.dispose();
this._target.dispose();
super.dispose();
}
this._isDisposed = true;
}
}
class CompositeMouseTracker extends Widget {
private _isMouseIn: boolean = false;
private _mouseTimeout: number | undefined;
private readonly _onMouseOut = new Emitter<void>();
get onMouseOut(): Event<void> { return this._onMouseOut.event; }
constructor(
private _elements: HTMLElement[]
) {
super();
this._elements.forEach(n => this.onmouseover(n, () => this._onTargetMouseOver()));
this._elements.forEach(n => this.onnonbubblingmouseout(n, () => this._onTargetMouseOut()));
}
private _onTargetMouseOver(): void {
this._isMouseIn = true;
this._clearEvaluateMouseStateTimeout();
}
private _onTargetMouseOut(): void {
this._isMouseIn = false;
this._evaluateMouseState();
}
private _evaluateMouseState(): void {
this._clearEvaluateMouseStateTimeout();
// Evaluate whether the mouse is still outside asynchronously such that other mouse targets
// have the opportunity to first their mouse in event.
this._mouseTimeout = window.setTimeout(() => this._fireIfMouseOutside(), 0);
}
private _clearEvaluateMouseStateTimeout(): void {
if (this._mouseTimeout) {
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 {
}
}