Path: blob/main/src/vs/base/browser/ui/dropdown/dropdown.ts
5243 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.button !== 0) {64// prevent right click trigger to allow separate context menu (https://github.com/microsoft/vscode/issues/151064)65return;66}6768if (this.visible) {69this.hide();70} else {71this.show();72}73}));74}7576this._register(addDisposableListener(this._label, EventType.KEY_DOWN, e => {77const event = new StandardKeyboardEvent(e);78if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {79EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/579978081if (this.visible) {82this.hide();83} else {84this.show();85}86}87}));8889const cleanupFn = labelRenderer(this._label);90if (cleanupFn) {91this._register(cleanupFn);92}9394this._register(Gesture.addTarget(this._label));95}9697get element(): HTMLElement {98return this._element;99}100101get label() {102return this._label;103}104105set tooltip(tooltip: string) {106if (this._label) {107if (!this.hover && tooltip !== '') {108this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this._label, tooltip));109} else if (this.hover) {110this.hover.update(tooltip);111}112}113}114115show(): void {116if (!this.visible) {117this.visible = true;118this._onDidChangeVisibility.fire(true);119}120}121122hide(): void {123if (this.visible) {124this.visible = false;125this._onDidChangeVisibility.fire(false);126}127}128129isVisible(): boolean {130return !!this.visible;131}132133protected onEvent(_e: Event, activeElement: HTMLElement): void {134this.hide();135}136137override dispose(): void {138super.dispose();139this.hide();140141if (this.boxContainer) {142this.boxContainer.remove();143this.boxContainer = undefined;144}145146if (this.contents) {147this.contents.remove();148this.contents = undefined;149}150151if (this._label) {152this._label.remove();153this._label = undefined;154}155}156}157158export interface IActionProvider {159getActions(): readonly IAction[];160}161162export function isActionProvider(obj: unknown): obj is IActionProvider {163const candidate = obj as IActionProvider | undefined;164165return typeof candidate?.getActions === 'function';166}167168export interface IDropdownMenuOptions extends IBaseDropdownOptions {169contextMenuProvider: IContextMenuProvider;170readonly actions?: IAction[];171readonly actionProvider?: IActionProvider;172menuClassName?: string;173menuAsChild?: boolean; // scope down for #99448174readonly skipTelemetry?: boolean;175}176177export class DropdownMenu extends BaseDropdown {178private _menuOptions: IMenuOptions | undefined;179private _actions: readonly IAction[] = [];180181constructor(container: HTMLElement, private readonly _options: IDropdownMenuOptions) {182super(container, _options);183184this.actions = _options.actions || [];185}186187set menuOptions(options: IMenuOptions | undefined) {188this._menuOptions = options;189}190191get menuOptions(): IMenuOptions | undefined {192return this._menuOptions;193}194195private get actions(): readonly IAction[] {196if (this._options.actionProvider) {197return this._options.actionProvider.getActions();198}199200return this._actions;201}202203private set actions(actions: readonly IAction[]) {204this._actions = actions;205}206207override show(): void {208super.show();209210this.element.classList.add('active');211212this._options.contextMenuProvider.showContextMenu({213getAnchor: () => this.element,214getActions: () => this.actions,215getActionsContext: () => this.menuOptions ? this.menuOptions.context : null,216getActionViewItem: (action, options) => this.menuOptions && this.menuOptions.actionViewItemProvider ? this.menuOptions.actionViewItemProvider(action, options) : undefined,217getKeyBinding: action => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : undefined,218getMenuClassName: () => this._options.menuClassName || '',219onHide: () => this.onHide(),220actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined,221anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT,222domForShadowRoot: this._options.menuAsChild ? this.element : undefined,223skipTelemetry: this._options.skipTelemetry224});225}226227override hide(): void {228super.hide();229}230231private onHide(): void {232this.hide();233this.element.classList.remove('active');234}235}236237238