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
5222 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
130
if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) {
131
return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor
132
}
133
134
135
return 0; // sad case, lay it over the anchor
136
}
137
}
138
139
export class ContextView extends Disposable {
140
141
private static readonly BUBBLE_UP_EVENTS = ['click', 'keydown', 'focus', 'blur'];
142
private static readonly BUBBLE_DOWN_EVENTS = ['click'];
143
144
private container: HTMLElement | null = null;
145
private view: HTMLElement;
146
private useFixedPosition = false;
147
private useShadowDOM = false;
148
private delegate: IDelegate | null = null;
149
private toDisposeOnClean: IDisposable = Disposable.None;
150
private toDisposeOnSetContainer: IDisposable = Disposable.None;
151
private shadowRoot: ShadowRoot | null = null;
152
private shadowRootHostElement: HTMLElement | null = null;
153
154
constructor(container: HTMLElement, domPosition: ContextViewDOMPosition) {
155
super();
156
157
this.view = DOM.$('.context-view');
158
DOM.hide(this.view);
159
160
this.setContainer(container, domPosition);
161
this._register(toDisposable(() => this.setContainer(null, ContextViewDOMPosition.ABSOLUTE)));
162
}
163
164
setContainer(container: HTMLElement | null, domPosition: ContextViewDOMPosition): void {
165
this.useFixedPosition = domPosition !== ContextViewDOMPosition.ABSOLUTE;
166
const usedShadowDOM = this.useShadowDOM;
167
this.useShadowDOM = domPosition === ContextViewDOMPosition.FIXED_SHADOW;
168
169
if (container === this.container && usedShadowDOM === this.useShadowDOM) {
170
return; // container is the same and no shadow DOM usage has changed
171
}
172
173
if (this.container) {
174
this.toDisposeOnSetContainer.dispose();
175
176
this.view.remove();
177
if (this.shadowRoot) {
178
this.shadowRoot = null;
179
this.shadowRootHostElement?.remove();
180
this.shadowRootHostElement = null;
181
}
182
183
this.container = null;
184
}
185
186
if (container) {
187
this.container = container;
188
189
if (this.useShadowDOM) {
190
this.shadowRootHostElement = DOM.$('.shadow-root-host');
191
this.container.appendChild(this.shadowRootHostElement);
192
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' });
193
const style = document.createElement('style');
194
style.textContent = SHADOW_ROOT_CSS;
195
this.shadowRoot.appendChild(style);
196
this.shadowRoot.appendChild(this.view);
197
this.shadowRoot.appendChild(DOM.$('slot'));
198
} else {
199
this.container.appendChild(this.view);
200
}
201
202
const toDisposeOnSetContainer = new DisposableStore();
203
204
ContextView.BUBBLE_UP_EVENTS.forEach(event => {
205
toDisposeOnSetContainer.add(DOM.addStandardDisposableListener(this.container!, event, e => {
206
this.onDOMEvent(e, false);
207
}));
208
});
209
210
ContextView.BUBBLE_DOWN_EVENTS.forEach(event => {
211
toDisposeOnSetContainer.add(DOM.addStandardDisposableListener(this.container!, event, e => {
212
this.onDOMEvent(e, true);
213
}, true));
214
});
215
216
this.toDisposeOnSetContainer = toDisposeOnSetContainer;
217
}
218
}
219
220
show(delegate: IDelegate): void {
221
if (this.isVisible()) {
222
this.hide();
223
}
224
225
// Show static box
226
DOM.clearNode(this.view);
227
this.view.className = 'context-view monaco-component';
228
this.view.style.top = '0px';
229
this.view.style.left = '0px';
230
this.view.style.zIndex = `${2575 + (delegate.layer ?? 0)}`;
231
this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute';
232
DOM.show(this.view);
233
234
// Render content
235
this.toDisposeOnClean = delegate.render(this.view) || Disposable.None;
236
237
// Set active delegate
238
this.delegate = delegate;
239
240
// Layout
241
this.doLayout();
242
243
// Focus
244
this.delegate.focus?.();
245
}
246
247
getViewElement(): HTMLElement {
248
return this.view;
249
}
250
251
layout(): void {
252
if (!this.isVisible()) {
253
return;
254
}
255
256
if (this.delegate!.canRelayout === false && !(platform.isIOS && BrowserFeatures.pointerEvents)) {
257
this.hide();
258
return;
259
}
260
261
this.delegate?.layout?.();
262
263
this.doLayout();
264
}
265
266
private doLayout(): void {
267
// Check that we still have a delegate - this.delegate.layout may have hidden
268
if (!this.isVisible()) {
269
return;
270
}
271
272
// Get anchor
273
const anchor = this.delegate!.getAnchor();
274
275
// Compute around
276
let around: IView;
277
278
// Get the element's position and size (to anchor the view)
279
if (DOM.isHTMLElement(anchor)) {
280
const elementPosition = DOM.getDomNodePagePosition(anchor);
281
282
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
283
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
284
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
285
const zoom = DOM.getDomNodeZoomLevel(anchor);
286
287
around = {
288
top: elementPosition.top * zoom,
289
left: elementPosition.left * zoom,
290
width: elementPosition.width * zoom,
291
height: elementPosition.height * zoom
292
};
293
} else if (isAnchor(anchor)) {
294
around = {
295
top: anchor.y,
296
left: anchor.x,
297
width: anchor.width || 1,
298
height: anchor.height || 2
299
};
300
} else {
301
around = {
302
top: anchor.posy,
303
left: anchor.posx,
304
// We are about to position the context view where the mouse
305
// cursor is. To prevent the view being exactly under the mouse
306
// when showing and thus potentially triggering an action within,
307
// we treat the mouse location like a small sized block element.
308
width: 2,
309
height: 2
310
};
311
}
312
313
const viewSizeWidth = DOM.getTotalWidth(this.view);
314
const viewSizeHeight = DOM.getTotalHeight(this.view);
315
316
const anchorPosition = this.delegate!.anchorPosition ?? AnchorPosition.BELOW;
317
const anchorAlignment = this.delegate!.anchorAlignment ?? AnchorAlignment.LEFT;
318
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL;
319
320
let top: number;
321
let left: number;
322
323
const activeWindow = DOM.getActiveWindow();
324
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
325
const verticalAnchor: ILayoutAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
326
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
327
328
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
329
330
// if view intersects vertically with anchor, we must avoid the anchor
331
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
332
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
333
}
334
335
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
336
} else {
337
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
338
const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
339
340
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
341
342
// if view intersects horizontally with anchor, we must avoid the anchor
343
if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
344
verticalAnchor.mode = LayoutAnchorMode.AVOID;
345
}
346
347
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
348
}
349
350
this.view.classList.remove('top', 'bottom', 'left', 'right');
351
this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
352
this.view.classList.add(anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
353
this.view.classList.toggle('fixed', this.useFixedPosition);
354
355
const containerPosition = DOM.getDomNodePagePosition(this.container!);
356
357
// Account for container scroll when positioning the context view
358
const containerScrollTop = this.container!.scrollTop || 0;
359
const containerScrollLeft = this.container!.scrollLeft || 0;
360
361
this.view.style.top = `${top - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).top : containerPosition.top) + containerScrollTop}px`;
362
this.view.style.left = `${left - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).left : containerPosition.left) + containerScrollLeft}px`;
363
this.view.style.width = 'initial';
364
}
365
366
hide(data?: unknown): void {
367
const delegate = this.delegate;
368
this.delegate = null;
369
370
if (delegate?.onHide) {
371
delegate.onHide(data);
372
}
373
374
this.toDisposeOnClean.dispose();
375
376
DOM.hide(this.view);
377
}
378
379
private isVisible(): boolean {
380
return !!this.delegate;
381
}
382
383
private onDOMEvent(e: UIEvent, onCapture: boolean): void {
384
if (this.delegate) {
385
if (this.delegate.onDOMEvent) {
386
this.delegate.onDOMEvent(e, <HTMLElement>DOM.getWindow(e).document.activeElement);
387
} else if (onCapture && !DOM.isAncestor(<HTMLElement>e.target, this.container)) {
388
this.hide();
389
}
390
}
391
}
392
393
override dispose(): void {
394
this.hide();
395
396
super.dispose();
397
}
398
}
399
400
const SHADOW_ROOT_CSS = /* css */ `
401
:host {
402
all: initial; /* 1st rule so subsequent properties are reset. */
403
}
404
405
.codicon[class*='codicon-'] {
406
font: normal normal normal 16px/1 codicon;
407
display: inline-block;
408
text-decoration: none;
409
text-rendering: auto;
410
text-align: center;
411
-webkit-font-smoothing: antialiased;
412
-moz-osx-font-smoothing: grayscale;
413
user-select: none;
414
-webkit-user-select: none;
415
-ms-user-select: none;
416
}
417
418
:host {
419
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", system-ui, "Ubuntu", "Droid Sans", sans-serif;
420
}
421
422
:host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
423
:host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; }
424
:host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; }
425
:host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; }
426
:host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Nanum Gothic", "AppleGothic", sans-serif; }
427
428
:host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; }
429
:host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; }
430
:host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; }
431
:host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; }
432
:host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; }
433
434
:host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; }
435
: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; }
436
: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; }
437
: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; }
438
: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; }
439
`;
440
441