Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/dropdown/dropdown.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 { IContextMenuProvider } from '../../contextmenu.js';
7
import { $, addDisposableListener, append, EventHelper, EventType, isMouseEvent } from '../../dom.js';
8
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
9
import { EventType as GestureEventType, Gesture } from '../../touch.js';
10
import { AnchorAlignment } from '../contextview/contextview.js';
11
import type { IManagedHover } from '../hover/hover.js';
12
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
13
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
14
import { IMenuOptions } from '../menu/menu.js';
15
import { ActionRunner, IAction } from '../../../common/actions.js';
16
import { Emitter } from '../../../common/event.js';
17
import { KeyCode } from '../../../common/keyCodes.js';
18
import { IDisposable } from '../../../common/lifecycle.js';
19
import './dropdown.css';
20
21
export interface ILabelRenderer {
22
(container: HTMLElement): IDisposable | null;
23
}
24
25
export interface IBaseDropdownOptions {
26
label?: string;
27
labelRenderer?: ILabelRenderer;
28
}
29
30
export class BaseDropdown extends ActionRunner {
31
private _element: HTMLElement;
32
private boxContainer?: HTMLElement;
33
private _label?: HTMLElement;
34
private contents?: HTMLElement;
35
36
private visible: boolean | undefined;
37
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
38
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
39
40
private hover: IManagedHover | undefined;
41
42
constructor(container: HTMLElement, options: IBaseDropdownOptions) {
43
super();
44
45
this._element = append(container, $('.monaco-dropdown'));
46
47
this._label = append(this._element, $('.dropdown-label'));
48
49
let labelRenderer = options.labelRenderer;
50
if (!labelRenderer) {
51
labelRenderer = (container: HTMLElement): IDisposable | null => {
52
container.textContent = options.label || '';
53
54
return null;
55
};
56
}
57
58
for (const event of [EventType.CLICK, EventType.MOUSE_DOWN, GestureEventType.Tap]) {
59
this._register(addDisposableListener(this.element, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger
60
}
61
62
for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) {
63
this._register(addDisposableListener(this._label, event, e => {
64
if (isMouseEvent(e) && (e.detail > 1 || e.button !== 0)) {
65
// prevent right click trigger to allow separate context menu (https://github.com/microsoft/vscode/issues/151064)
66
// prevent multiple clicks to open multiple context menus (https://github.com/microsoft/vscode/issues/41363)
67
return;
68
}
69
70
if (this.visible) {
71
this.hide();
72
} else {
73
this.show();
74
}
75
}));
76
}
77
78
this._register(addDisposableListener(this._label, EventType.KEY_DOWN, e => {
79
const event = new StandardKeyboardEvent(e);
80
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
81
EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/57997
82
83
if (this.visible) {
84
this.hide();
85
} else {
86
this.show();
87
}
88
}
89
}));
90
91
const cleanupFn = labelRenderer(this._label);
92
if (cleanupFn) {
93
this._register(cleanupFn);
94
}
95
96
this._register(Gesture.addTarget(this._label));
97
}
98
99
get element(): HTMLElement {
100
return this._element;
101
}
102
103
get label() {
104
return this._label;
105
}
106
107
set tooltip(tooltip: string) {
108
if (this._label) {
109
if (!this.hover && tooltip !== '') {
110
this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this._label, tooltip));
111
} else if (this.hover) {
112
this.hover.update(tooltip);
113
}
114
}
115
}
116
117
show(): void {
118
if (!this.visible) {
119
this.visible = true;
120
this._onDidChangeVisibility.fire(true);
121
}
122
}
123
124
hide(): void {
125
if (this.visible) {
126
this.visible = false;
127
this._onDidChangeVisibility.fire(false);
128
}
129
}
130
131
isVisible(): boolean {
132
return !!this.visible;
133
}
134
135
protected onEvent(_e: Event, activeElement: HTMLElement): void {
136
this.hide();
137
}
138
139
override dispose(): void {
140
super.dispose();
141
this.hide();
142
143
if (this.boxContainer) {
144
this.boxContainer.remove();
145
this.boxContainer = undefined;
146
}
147
148
if (this.contents) {
149
this.contents.remove();
150
this.contents = undefined;
151
}
152
153
if (this._label) {
154
this._label.remove();
155
this._label = undefined;
156
}
157
}
158
}
159
160
export interface IActionProvider {
161
getActions(): readonly IAction[];
162
}
163
164
export function isActionProvider(obj: unknown): obj is IActionProvider {
165
const candidate = obj as IActionProvider | undefined;
166
167
return typeof candidate?.getActions === 'function';
168
}
169
170
export interface IDropdownMenuOptions extends IBaseDropdownOptions {
171
contextMenuProvider: IContextMenuProvider;
172
readonly actions?: IAction[];
173
readonly actionProvider?: IActionProvider;
174
menuClassName?: string;
175
menuAsChild?: boolean; // scope down for #99448
176
readonly skipTelemetry?: boolean;
177
}
178
179
export class DropdownMenu extends BaseDropdown {
180
private _menuOptions: IMenuOptions | undefined;
181
private _actions: readonly IAction[] = [];
182
183
constructor(container: HTMLElement, private readonly _options: IDropdownMenuOptions) {
184
super(container, _options);
185
186
this.actions = _options.actions || [];
187
}
188
189
set menuOptions(options: IMenuOptions | undefined) {
190
this._menuOptions = options;
191
}
192
193
get menuOptions(): IMenuOptions | undefined {
194
return this._menuOptions;
195
}
196
197
private get actions(): readonly IAction[] {
198
if (this._options.actionProvider) {
199
return this._options.actionProvider.getActions();
200
}
201
202
return this._actions;
203
}
204
205
private set actions(actions: readonly IAction[]) {
206
this._actions = actions;
207
}
208
209
override show(): void {
210
super.show();
211
212
this.element.classList.add('active');
213
214
this._options.contextMenuProvider.showContextMenu({
215
getAnchor: () => this.element,
216
getActions: () => this.actions,
217
getActionsContext: () => this.menuOptions ? this.menuOptions.context : null,
218
getActionViewItem: (action, options) => this.menuOptions && this.menuOptions.actionViewItemProvider ? this.menuOptions.actionViewItemProvider(action, options) : undefined,
219
getKeyBinding: action => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : undefined,
220
getMenuClassName: () => this._options.menuClassName || '',
221
onHide: () => this.onHide(),
222
actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined,
223
anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT,
224
domForShadowRoot: this._options.menuAsChild ? this.element : undefined,
225
skipTelemetry: this._options.skipTelemetry
226
});
227
}
228
229
override hide(): void {
230
super.hide();
231
}
232
233
private onHide(): void {
234
this.hide();
235
this.element.classList.remove('active');
236
}
237
}
238
239