Path: blob/main/src/vs/base/browser/ui/dropdown/dropdown.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IContextMenuProvider } from '../../contextmenu.js';6import { $, addDisposableListener, append, EventHelper, EventType, isMouseEvent } from '../../dom.js';7import { StandardKeyboardEvent } from '../../keyboardEvent.js';8import { EventType as GestureEventType, Gesture } from '../../touch.js';9import { AnchorAlignment } from '../contextview/contextview.js';10import type { IManagedHover } from '../hover/hover.js';11import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';12import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';13import { IMenuOptions } from '../menu/menu.js';14import { ActionRunner, IAction } from '../../../common/actions.js';15import { Emitter } from '../../../common/event.js';16import { KeyCode } from '../../../common/keyCodes.js';17import { IDisposable } from '../../../common/lifecycle.js';18import './dropdown.css';1920export interface ILabelRenderer {21(container: HTMLElement): IDisposable | null;22}2324export interface IBaseDropdownOptions {25label?: string;26labelRenderer?: ILabelRenderer;27}2829export class BaseDropdown extends ActionRunner {30private _element: HTMLElement;31private boxContainer?: HTMLElement;32private _label?: HTMLElement;33private contents?: HTMLElement;3435private visible: boolean | undefined;36private _onDidChangeVisibility = this._register(new Emitter<boolean>());37readonly onDidChangeVisibility = this._onDidChangeVisibility.event;3839private hover: IManagedHover | undefined;4041constructor(container: HTMLElement, options: IBaseDropdownOptions) {42super();4344this._element = append(container, $('.monaco-dropdown'));4546this._label = append(this._element, $('.dropdown-label'));4748let labelRenderer = options.labelRenderer;49if (!labelRenderer) {50labelRenderer = (container: HTMLElement): IDisposable | null => {51container.textContent = options.label || '';5253return null;54};55}5657for (const event of [EventType.CLICK, EventType.MOUSE_DOWN, GestureEventType.Tap]) {58this._register(addDisposableListener(this.element, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger59}6061for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) {62this._register(addDisposableListener(this._label, event, e => {63if (isMouseEvent(e) && (e.detail > 1 || e.button !== 0)) {64// prevent right click trigger to allow separate context menu (https://github.com/microsoft/vscode/issues/151064)65// prevent multiple clicks to open multiple context menus (https://github.com/microsoft/vscode/issues/41363)66return;67}6869if (this.visible) {70this.hide();71} else {72this.show();73}74}));75}7677this._register(addDisposableListener(this._label, EventType.KEY_DOWN, e => {78const event = new StandardKeyboardEvent(e);79if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {80EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/579978182if (this.visible) {83this.hide();84} else {85this.show();86}87}88}));8990const cleanupFn = labelRenderer(this._label);91if (cleanupFn) {92this._register(cleanupFn);93}9495this._register(Gesture.addTarget(this._label));96}9798get element(): HTMLElement {99return this._element;100}101102get label() {103return this._label;104}105106set tooltip(tooltip: string) {107if (this._label) {108if (!this.hover && tooltip !== '') {109this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this._label, tooltip));110} else if (this.hover) {111this.hover.update(tooltip);112}113}114}115116show(): void {117if (!this.visible) {118this.visible = true;119this._onDidChangeVisibility.fire(true);120}121}122123hide(): void {124if (this.visible) {125this.visible = false;126this._onDidChangeVisibility.fire(false);127}128}129130isVisible(): boolean {131return !!this.visible;132}133134protected onEvent(_e: Event, activeElement: HTMLElement): void {135this.hide();136}137138override dispose(): void {139super.dispose();140this.hide();141142if (this.boxContainer) {143this.boxContainer.remove();144this.boxContainer = undefined;145}146147if (this.contents) {148this.contents.remove();149this.contents = undefined;150}151152if (this._label) {153this._label.remove();154this._label = undefined;155}156}157}158159export interface IActionProvider {160getActions(): readonly IAction[];161}162163export function isActionProvider(obj: unknown): obj is IActionProvider {164const candidate = obj as IActionProvider | undefined;165166return typeof candidate?.getActions === 'function';167}168169export interface IDropdownMenuOptions extends IBaseDropdownOptions {170contextMenuProvider: IContextMenuProvider;171readonly actions?: IAction[];172readonly actionProvider?: IActionProvider;173menuClassName?: string;174menuAsChild?: boolean; // scope down for #99448175readonly skipTelemetry?: boolean;176}177178export class DropdownMenu extends BaseDropdown {179private _menuOptions: IMenuOptions | undefined;180private _actions: readonly IAction[] = [];181182constructor(container: HTMLElement, private readonly _options: IDropdownMenuOptions) {183super(container, _options);184185this.actions = _options.actions || [];186}187188set menuOptions(options: IMenuOptions | undefined) {189this._menuOptions = options;190}191192get menuOptions(): IMenuOptions | undefined {193return this._menuOptions;194}195196private get actions(): readonly IAction[] {197if (this._options.actionProvider) {198return this._options.actionProvider.getActions();199}200201return this._actions;202}203204private set actions(actions: readonly IAction[]) {205this._actions = actions;206}207208override show(): void {209super.show();210211this.element.classList.add('active');212213this._options.contextMenuProvider.showContextMenu({214getAnchor: () => this.element,215getActions: () => this.actions,216getActionsContext: () => this.menuOptions ? this.menuOptions.context : null,217getActionViewItem: (action, options) => this.menuOptions && this.menuOptions.actionViewItemProvider ? this.menuOptions.actionViewItemProvider(action, options) : undefined,218getKeyBinding: action => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : undefined,219getMenuClassName: () => this._options.menuClassName || '',220onHide: () => this.onHide(),221actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined,222anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT,223domForShadowRoot: this._options.menuAsChild ? this.element : undefined,224skipTelemetry: this._options.skipTelemetry225});226}227228override hide(): void {229super.hide();230}231232private onHide(): void {233this.hide();234this.element.classList.remove('active');235}236}237238239