Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/contextview/contextview.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 { BrowserFeatures } from '../../canIUse.js';
7
import * as DOM from '../../dom.js';
8
import { StandardMouseEvent } from '../../mouseEvent.js';
9
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js';
10
import * as platform from '../../../common/platform.js';
11
import { Range } from '../../../common/range.js';
12
import { OmitOptional } from '../../../common/types.js';
13
import './contextview.css';
14
15
export const enum ContextViewDOMPosition {
16
ABSOLUTE = 1,
17
FIXED,
18
FIXED_SHADOW
19
}
20
21
export interface IAnchor {
22
x: number;
23
y: number;
24
width?: number;
25
height?: number;
26
}
27
28
export function isAnchor(obj: unknown): obj is IAnchor | OmitOptional<IAnchor> {
29
const anchor = obj as IAnchor | OmitOptional<IAnchor> | undefined;
30
31
return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number';
32
}
33
34
export const enum AnchorAlignment {
35
LEFT, RIGHT
36
}
37
38
export const enum AnchorPosition {
39
BELOW, ABOVE
40
}
41
42
export const enum AnchorAxisAlignment {
43
VERTICAL, HORIZONTAL
44
}
45
46
export interface IDelegate {
47
/**
48
* The anchor where to position the context view.
49
* Use a `HTMLElement` to position the view at the element,
50
* a `StandardMouseEvent` to position it at the mouse position
51
* or an `IAnchor` to position it at a specific location.
52
*/
53
getAnchor(): HTMLElement | StandardMouseEvent | IAnchor;
54
render(container: HTMLElement): IDisposable | null;
55
focus?(): void;
56
layout?(): void;
57
anchorAlignment?: AnchorAlignment; // default: left
58
anchorPosition?: AnchorPosition; // default: below
59
anchorAxisAlignment?: AnchorAxisAlignment; // default: vertical
60
canRelayout?: boolean; // default: true
61
onDOMEvent?(e: Event, activeElement: HTMLElement): void;
62
onHide?(data?: unknown): void;
63
64
/**
65
* context views with higher layers are rendered higher in z-index order
66
*/
67
layer?: number; // Default: 0
68
}
69
70
export interface IContextViewProvider {
71
showContextView(delegate: IDelegate, container?: HTMLElement): void;
72
hideContextView(): void;
73
layout(): void;
74
}
75
76
export interface IPosition {
77
top: number;
78
left: number;
79
}
80
81
export interface ISize {
82
width: number;
83
height: number;
84
}
85
86
export interface IView extends IPosition, ISize { }
87
88
export const enum LayoutAnchorPosition {
89
Before,
90
After
91
}
92
93
export enum LayoutAnchorMode {
94
AVOID,
95
ALIGN
96
}
97
98
export interface ILayoutAnchor {
99
offset: number;
100
size: number;
101
mode?: LayoutAnchorMode; // default: AVOID
102
position: LayoutAnchorPosition;
103
}
104
105
/**
106
* Lays out a one dimensional view next to an anchor in a viewport.
107
*
108
* @returns The view offset within the viewport.
109
*/
110
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number {
111
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
112
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
113
114
if (anchor.position === LayoutAnchorPosition.Before) {
115
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
116
return layoutAfterAnchorBoundary; // happy case, lay it out after the anchor
117
}
118
119
if (viewSize <= layoutBeforeAnchorBoundary) {
120
return layoutBeforeAnchorBoundary - viewSize; // ok case, lay it out before the anchor
121
}
122
123
return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor
124
} else {
125
if (viewSize <= layoutBeforeAnchorBoundary) {
126
return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor
127
}
128
129
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
130
return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor
131
}
132
133
return 0; // sad case, lay it over the anchor
134
}
135
}
136
137
export class ContextView extends Disposable {
138
139
private static readonly BUBBLE_UP_EVENTS = ['click', 'keydown', 'focus', 'blur'];
140
private static readonly BUBBLE_DOWN_EVENTS = ['click'];
141
142
private container: HTMLElement | null = null;
143
private view: HTMLElement;
144
private useFixedPosition = false;
145
private useShadowDOM = false;
146
private delegate: IDelegate | null = null;
147
private toDisposeOnClean: IDisposable = Disposable.None;
148
private toDisposeOnSetContainer: IDisposable = Disposable.None;
149
private shadowRoot: ShadowRoot | null = null;
150
private shadowRootHostElement: HTMLElement | null = null;
151
152
constructor(container: HTMLElement, domPosition: ContextViewDOMPosition) {
153
super();
154
155
this.view = DOM.$('.context-view');
156
DOM.hide(this.view);
157
158
this.setContainer(container, domPosition);
159
this._register(toDisposable(() => this.setContainer(null, ContextViewDOMPosition.ABSOLUTE)));
160
}
161
162
setContainer(container: HTMLElement | null, domPosition: ContextViewDOMPosition): void {
163
this.useFixedPosition = domPosition !== ContextViewDOMPosition.ABSOLUTE;
164
const usedShadowDOM = this.useShadowDOM;
165
this.useShadowDOM = domPosition === ContextViewDOMPosition.FIXED_SHADOW;
166
167
if (container === this.container && usedShadowDOM === this.useShadowDOM) {
168
return; // container is the same and no shadow DOM usage has changed
169
}
170
171
if (this.container) {
172
this.toDisposeOnSetContainer.dispose();
173
174
this.view.remove();
175
if (this.shadowRoot) {
176
this.shadowRoot = null;
177
this.shadowRootHostElement?.remove();
178
this.shadowRootHostElement = null;
179
}
180
181
this.container = null;
182
}
183
184
if (container) {
185
this.container = container;
186
187
if (this.useShadowDOM) {
188
this.shadowRootHostElement = DOM.$('.shadow-root-host');
189
this.container.appendChild(this.shadowRootHostElement);
190
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' });
191
const style = document.createElement('style');
192
style.textContent = SHADOW_ROOT_CSS;
193
this.shadowRoot.appendChild(style);
194
this.shadowRoot.appendChild(this.view);
195
this.shadowRoot.appendChild(DOM.$('slot'));
196
} else {
197
this.container.appendChild(this.view);
198
}
199
200
const toDisposeOnSetContainer = new DisposableStore();
201
202
ContextView.BUBBLE_UP_EVENTS.forEach(event => {
203
toDisposeOnSetContainer.add(DOM.addStandardDisposableListener(this.container!, event, e => {
204
this.onDOMEvent(e, false);
205
}));
206
});
207
208
ContextView.BUBBLE_DOWN_EVENTS.forEach(event => {
209
toDisposeOnSetContainer.add(DOM.addStandardDisposableListener(this.container!, event, e => {
210
this.onDOMEvent(e, true);
211
}, true));
212
});
213
214
this.toDisposeOnSetContainer = toDisposeOnSetContainer;
215
}
216
}
217
218
show(delegate: IDelegate): void {
219
if (this.isVisible()) {
220
this.hide();
221
}
222
223
// Show static box
224
DOM.clearNode(this.view);
225
this.view.className = 'context-view monaco-component';
226
this.view.style.top = '0px';
227
this.view.style.left = '0px';
228
this.view.style.zIndex = `${2575 + (delegate.layer ?? 0)}`;
229
this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute';
230
DOM.show(this.view);
231
232
// Render content
233
this.toDisposeOnClean = delegate.render(this.view) || Disposable.None;
234
235
// Set active delegate
236
this.delegate = delegate;
237
238
// Layout
239
this.doLayout();
240
241
// Focus
242
this.delegate.focus?.();
243
}
244
245
getViewElement(): HTMLElement {
246
return this.view;
247
}
248
249
layout(): void {
250
if (!this.isVisible()) {
251
return;
252
}
253
254
if (this.delegate!.canRelayout === false && !(platform.isIOS && BrowserFeatures.pointerEvents)) {
255
this.hide();
256
return;
257
}
258
259
this.delegate?.layout?.();
260
261
this.doLayout();
262
}
263
264
private doLayout(): void {
265
// Check that we still have a delegate - this.delegate.layout may have hidden
266
if (!this.isVisible()) {
267
return;
268
}
269
270
// Get anchor
271
const anchor = this.delegate!.getAnchor();
272
273
// Compute around
274
let around: IView;
275
276
// Get the element's position and size (to anchor the view)
277
if (DOM.isHTMLElement(anchor)) {
278
const elementPosition = DOM.getDomNodePagePosition(anchor);
279
280
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
281
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
282
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
283
const zoom = DOM.getDomNodeZoomLevel(anchor);
284
285
around = {
286
top: elementPosition.top * zoom,
287
left: elementPosition.left * zoom,
288
width: elementPosition.width * zoom,
289
height: elementPosition.height * zoom
290
};
291
} else if (isAnchor(anchor)) {
292
around = {
293
top: anchor.y,
294
left: anchor.x,
295
width: anchor.width || 1,
296
height: anchor.height || 2
297
};
298
} else {
299
around = {
300
top: anchor.posy,
301
left: anchor.posx,
302
// We are about to position the context view where the mouse
303
// cursor is. To prevent the view being exactly under the mouse
304
// when showing and thus potentially triggering an action within,
305
// we treat the mouse location like a small sized block element.
306
width: 2,
307
height: 2
308
};
309
}
310
311
const viewSizeWidth = DOM.getTotalWidth(this.view);
312
const viewSizeHeight = DOM.getTotalHeight(this.view);
313
314
const anchorPosition = this.delegate!.anchorPosition ?? AnchorPosition.BELOW;
315
const anchorAlignment = this.delegate!.anchorAlignment ?? AnchorAlignment.LEFT;
316
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL;
317
318
let top: number;
319
let left: number;
320
321
const activeWindow = DOM.getActiveWindow();
322
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
323
const verticalAnchor: ILayoutAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
324
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
325
326
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
327
328
// if view intersects vertically with anchor, we must avoid the anchor
329
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
330
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
331
}
332
333
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
334
} else {
335
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
336
const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
337
338
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
339
340
// if view intersects horizontally with anchor, we must avoid the anchor
341
if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
342
verticalAnchor.mode = LayoutAnchorMode.AVOID;
343
}
344
345
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
346
}
347
348
this.view.classList.remove('top', 'bottom', 'left', 'right');
349
this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
350
this.view.classList.add(anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
351
this.view.classList.toggle('fixed', this.useFixedPosition);
352
353
const containerPosition = DOM.getDomNodePagePosition(this.container!);
354
355
// Account for container scroll when positioning the context view
356
const containerScrollTop = this.container!.scrollTop || 0;
357
const containerScrollLeft = this.container!.scrollLeft || 0;
358
359
this.view.style.top = `${top - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).top : containerPosition.top) + containerScrollTop}px`;
360
this.view.style.left = `${left - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).left : containerPosition.left) + containerScrollLeft}px`;
361
this.view.style.width = 'initial';
362
}
363
364
hide(data?: unknown): void {
365
const delegate = this.delegate;
366
this.delegate = null;
367
368
if (delegate?.onHide) {
369
delegate.onHide(data);
370
}
371
372
this.toDisposeOnClean.dispose();
373
374
DOM.hide(this.view);
375
}
376
377
private isVisible(): boolean {
378
return !!this.delegate;
379
}
380
381
private onDOMEvent(e: UIEvent, onCapture: boolean): void {
382
if (this.delegate) {
383
if (this.delegate.onDOMEvent) {
384
this.delegate.onDOMEvent(e, <HTMLElement>DOM.getWindow(e).document.activeElement);
385
} else if (onCapture && !DOM.isAncestor(<HTMLElement>e.target, this.container)) {
386
this.hide();
387
}
388
}
389
}
390
391
override dispose(): void {
392
this.hide();
393
394
super.dispose();
395
}
396
}
397
398
const SHADOW_ROOT_CSS = /* css */ `
399
:host {
400
all: initial; /* 1st rule so subsequent properties are reset. */
401
}
402
403
.codicon[class*='codicon-'] {
404
font: normal normal normal 16px/1 codicon;
405
display: inline-block;
406
text-decoration: none;
407
text-rendering: auto;
408
text-align: center;
409
-webkit-font-smoothing: antialiased;
410
-moz-osx-font-smoothing: grayscale;
411
user-select: none;
412
-webkit-user-select: none;
413
-ms-user-select: none;
414
}
415
416
:host {
417
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", system-ui, "Ubuntu", "Droid Sans", sans-serif;
418
}
419
420
:host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
421
:host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; }
422
:host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; }
423
:host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; }
424
:host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Nanum Gothic", "AppleGothic", sans-serif; }
425
426
:host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; }
427
:host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; }
428
:host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; }
429
:host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; }
430
:host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; }
431
432
:host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; }
433
:host-context(.linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; }
434
:host-context(.linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; }
435
:host-context(.linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; }
436
:host-context(.linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; }
437
`;
438
439