mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-02 23:05:15 +01:00
457 lines
14 KiB
TypeScript
457 lines
14 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
|
import { addDisposableListener, EventHelper, EventType, IFocusTracker, reset, trackFocus } from 'vs/base/browser/dom';
|
|
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
|
import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch';
|
|
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
|
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
|
|
import { Codicon } from 'vs/base/common/codicons';
|
|
import { ThemeIcon } from 'vs/base/common/themables';
|
|
import { Color } from 'vs/base/common/color';
|
|
import { Emitter, Event as BaseEvent } from 'vs/base/common/event';
|
|
import { KeyCode } from 'vs/base/common/keyCodes';
|
|
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
|
import { localize } from 'vs/nls';
|
|
import 'vs/css!./button';
|
|
|
|
export interface IButtonOptions extends IButtonStyles {
|
|
readonly title?: boolean | string;
|
|
readonly supportIcons?: boolean;
|
|
readonly supportShortLabel?: boolean;
|
|
readonly secondary?: boolean;
|
|
}
|
|
|
|
export interface IButtonStyles {
|
|
readonly buttonBackground: string | undefined;
|
|
readonly buttonHoverBackground: string | undefined;
|
|
readonly buttonForeground: string | undefined;
|
|
readonly buttonSeparator: string | undefined;
|
|
readonly buttonSecondaryBackground: string | undefined;
|
|
readonly buttonSecondaryHoverBackground: string | undefined;
|
|
readonly buttonSecondaryForeground: string | undefined;
|
|
readonly buttonBorder: string | undefined;
|
|
}
|
|
|
|
export const unthemedButtonStyles: IButtonStyles = {
|
|
buttonBackground: '#0E639C',
|
|
buttonHoverBackground: '#006BB3',
|
|
buttonSeparator: Color.white.toString(),
|
|
buttonForeground: Color.white.toString(),
|
|
buttonBorder: undefined,
|
|
buttonSecondaryBackground: undefined,
|
|
buttonSecondaryForeground: undefined,
|
|
buttonSecondaryHoverBackground: undefined
|
|
};
|
|
|
|
export interface IButton extends IDisposable {
|
|
readonly element: HTMLElement;
|
|
readonly onDidClick: BaseEvent<Event | undefined>;
|
|
label: string;
|
|
icon: ThemeIcon;
|
|
enabled: boolean;
|
|
focus(): void;
|
|
hasFocus(): boolean;
|
|
}
|
|
|
|
export interface IButtonWithDescription extends IButton {
|
|
description: string;
|
|
}
|
|
|
|
export class Button extends Disposable implements IButton {
|
|
|
|
protected options: IButtonOptions;
|
|
protected _element: HTMLElement;
|
|
protected _labelElement: HTMLElement | undefined;
|
|
protected _labelShortElement: HTMLElement | undefined;
|
|
|
|
private _onDidClick = this._register(new Emitter<Event>());
|
|
get onDidClick(): BaseEvent<Event> { return this._onDidClick.event; }
|
|
|
|
private focusTracker: IFocusTracker;
|
|
|
|
constructor(container: HTMLElement, options: IButtonOptions) {
|
|
super();
|
|
|
|
this.options = options;
|
|
|
|
this._element = document.createElement('a');
|
|
this._element.classList.add('monaco-button');
|
|
this._element.tabIndex = 0;
|
|
this._element.setAttribute('role', 'button');
|
|
|
|
const background = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground;
|
|
const foreground = options.secondary ? options.buttonSecondaryForeground : options.buttonForeground;
|
|
|
|
this._element.style.color = foreground || '';
|
|
this._element.style.backgroundColor = background || '';
|
|
|
|
if (options.supportShortLabel) {
|
|
this._labelShortElement = document.createElement('div');
|
|
this._labelShortElement.classList.add('monaco-button-label-short');
|
|
this._element.appendChild(this._labelShortElement);
|
|
|
|
this._labelElement = document.createElement('div');
|
|
this._labelElement.classList.add('monaco-button-label');
|
|
this._element.appendChild(this._labelElement);
|
|
|
|
this._element.classList.add('monaco-text-button-with-short-label');
|
|
}
|
|
|
|
container.appendChild(this._element);
|
|
|
|
this._register(Gesture.addTarget(this._element));
|
|
|
|
[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {
|
|
this._register(addDisposableListener(this._element, eventType, e => {
|
|
if (!this.enabled) {
|
|
EventHelper.stop(e);
|
|
return;
|
|
}
|
|
|
|
this._onDidClick.fire(e);
|
|
}));
|
|
});
|
|
|
|
this._register(addDisposableListener(this._element, EventType.KEY_DOWN, e => {
|
|
const event = new StandardKeyboardEvent(e);
|
|
let eventHandled = false;
|
|
if (this.enabled && (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) {
|
|
this._onDidClick.fire(e);
|
|
eventHandled = true;
|
|
} else if (event.equals(KeyCode.Escape)) {
|
|
this._element.blur();
|
|
eventHandled = true;
|
|
}
|
|
|
|
if (eventHandled) {
|
|
EventHelper.stop(event, true);
|
|
}
|
|
}));
|
|
|
|
this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => {
|
|
if (!this._element.classList.contains('disabled')) {
|
|
this.updateBackground(true);
|
|
}
|
|
}));
|
|
|
|
this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => {
|
|
this.updateBackground(false); // restore standard styles
|
|
}));
|
|
|
|
// Also set hover background when button is focused for feedback
|
|
this.focusTracker = this._register(trackFocus(this._element));
|
|
this._register(this.focusTracker.onDidFocus(() => { if (this.enabled) { this.updateBackground(true); } }));
|
|
this._register(this.focusTracker.onDidBlur(() => { if (this.enabled) { this.updateBackground(false); } }));
|
|
}
|
|
|
|
private getContentElements(content: string): HTMLElement[] {
|
|
const elements: HTMLSpanElement[] = [];
|
|
for (let segment of renderLabelWithIcons(content)) {
|
|
if (typeof (segment) === 'string') {
|
|
segment = segment.trim();
|
|
|
|
// Ignore empty segment
|
|
if (segment === '') {
|
|
continue;
|
|
}
|
|
|
|
// Convert string segments to <span> nodes
|
|
const node = document.createElement('span');
|
|
node.textContent = segment;
|
|
elements.push(node);
|
|
} else {
|
|
elements.push(segment);
|
|
}
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
private updateBackground(hover: boolean): void {
|
|
let background;
|
|
if (this.options.secondary) {
|
|
background = hover ? this.options.buttonSecondaryHoverBackground : this.options.buttonSecondaryBackground;
|
|
} else {
|
|
background = hover ? this.options.buttonHoverBackground : this.options.buttonBackground;
|
|
}
|
|
if (background) {
|
|
this._element.style.backgroundColor = background;
|
|
}
|
|
}
|
|
|
|
get element(): HTMLElement {
|
|
return this._element;
|
|
}
|
|
|
|
set label(value: string) {
|
|
this._element.classList.add('monaco-text-button');
|
|
const labelElement = this.options.supportShortLabel ? this._labelElement! : this._element;
|
|
|
|
if (this.options.supportIcons) {
|
|
reset(labelElement, ...this.getContentElements(value));
|
|
} else {
|
|
labelElement.textContent = value;
|
|
}
|
|
|
|
if (typeof this.options.title === 'string') {
|
|
this._element.title = this.options.title;
|
|
} else if (this.options.title) {
|
|
this._element.title = value;
|
|
}
|
|
}
|
|
|
|
set labelShort(value: string) {
|
|
if (!this.options.supportShortLabel || !this._labelShortElement) {
|
|
return;
|
|
}
|
|
|
|
if (this.options.supportIcons) {
|
|
reset(this._labelShortElement, ...this.getContentElements(value));
|
|
} else {
|
|
this._labelShortElement.textContent = value;
|
|
}
|
|
}
|
|
|
|
set icon(icon: ThemeIcon) {
|
|
this._element.classList.add(...ThemeIcon.asClassNameArray(icon));
|
|
}
|
|
|
|
set enabled(value: boolean) {
|
|
if (value) {
|
|
this._element.classList.remove('disabled');
|
|
this._element.setAttribute('aria-disabled', String(false));
|
|
this._element.tabIndex = 0;
|
|
} else {
|
|
this._element.classList.add('disabled');
|
|
this._element.setAttribute('aria-disabled', String(true));
|
|
}
|
|
}
|
|
|
|
get enabled() {
|
|
return !this._element.classList.contains('disabled');
|
|
}
|
|
|
|
focus(): void {
|
|
this._element.focus();
|
|
}
|
|
|
|
hasFocus(): boolean {
|
|
return this._element === document.activeElement;
|
|
}
|
|
}
|
|
|
|
export interface IButtonWithDropdownOptions extends IButtonOptions {
|
|
readonly contextMenuProvider: IContextMenuProvider;
|
|
readonly actions: IAction[];
|
|
readonly actionRunner?: IActionRunner;
|
|
readonly addPrimaryActionToDropdown?: boolean;
|
|
}
|
|
|
|
export class ButtonWithDropdown extends Disposable implements IButton {
|
|
|
|
private readonly button: Button;
|
|
private readonly action: Action;
|
|
private readonly dropdownButton: Button;
|
|
private readonly separatorContainer: HTMLDivElement;
|
|
private readonly separator: HTMLDivElement;
|
|
|
|
readonly element: HTMLElement;
|
|
private readonly _onDidClick = this._register(new Emitter<Event | undefined>());
|
|
readonly onDidClick = this._onDidClick.event;
|
|
|
|
constructor(container: HTMLElement, options: IButtonWithDropdownOptions) {
|
|
super();
|
|
|
|
this.element = document.createElement('div');
|
|
this.element.classList.add('monaco-button-dropdown');
|
|
container.appendChild(this.element);
|
|
|
|
this.button = this._register(new Button(this.element, options));
|
|
this._register(this.button.onDidClick(e => this._onDidClick.fire(e)));
|
|
this.action = this._register(new Action('primaryAction', this.button.label, undefined, true, async () => this._onDidClick.fire(undefined)));
|
|
|
|
this.separatorContainer = document.createElement('div');
|
|
this.separatorContainer.classList.add('monaco-button-dropdown-separator');
|
|
|
|
this.separator = document.createElement('div');
|
|
this.separatorContainer.appendChild(this.separator);
|
|
this.element.appendChild(this.separatorContainer);
|
|
|
|
// Separator styles
|
|
const border = options.buttonBorder;
|
|
if (border) {
|
|
this.separatorContainer.style.borderTop = '1px solid ' + border;
|
|
this.separatorContainer.style.borderBottom = '1px solid ' + border;
|
|
}
|
|
|
|
const buttonBackground = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground;
|
|
this.separatorContainer.style.backgroundColor = buttonBackground ?? '';
|
|
this.separator.style.backgroundColor = options.buttonSeparator ?? '';
|
|
|
|
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true }));
|
|
this.dropdownButton.element.title = localize("button dropdown more actions", 'More Actions...');
|
|
this.dropdownButton.element.setAttribute('aria-haspopup', 'true');
|
|
this.dropdownButton.element.setAttribute('aria-expanded', 'false');
|
|
this.dropdownButton.element.classList.add('monaco-dropdown-button');
|
|
this.dropdownButton.icon = Codicon.dropDownButton;
|
|
this._register(this.dropdownButton.onDidClick(e => {
|
|
options.contextMenuProvider.showContextMenu({
|
|
getAnchor: () => this.dropdownButton.element,
|
|
getActions: () => options.addPrimaryActionToDropdown === false ? [...options.actions] : [this.action, ...options.actions],
|
|
actionRunner: options.actionRunner,
|
|
onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false')
|
|
});
|
|
this.dropdownButton.element.setAttribute('aria-expanded', 'true');
|
|
}));
|
|
}
|
|
|
|
set label(value: string) {
|
|
this.button.label = value;
|
|
this.action.label = value;
|
|
}
|
|
|
|
set icon(icon: ThemeIcon) {
|
|
this.button.icon = icon;
|
|
}
|
|
|
|
set enabled(enabled: boolean) {
|
|
this.button.enabled = enabled;
|
|
this.dropdownButton.enabled = enabled;
|
|
|
|
this.element.classList.toggle('disabled', !enabled);
|
|
}
|
|
|
|
get enabled(): boolean {
|
|
return this.button.enabled;
|
|
}
|
|
|
|
focus(): void {
|
|
this.button.focus();
|
|
}
|
|
|
|
hasFocus(): boolean {
|
|
return this.button.hasFocus() || this.dropdownButton.hasFocus();
|
|
}
|
|
}
|
|
|
|
export class ButtonWithDescription implements IButtonWithDescription {
|
|
private _button: Button;
|
|
private _element: HTMLElement;
|
|
private _descriptionElement: HTMLElement;
|
|
|
|
constructor(container: HTMLElement, private readonly options: IButtonOptions) {
|
|
this._element = document.createElement('div');
|
|
this._element.classList.add('monaco-description-button');
|
|
this._button = new Button(this._element, options);
|
|
|
|
this._descriptionElement = document.createElement('div');
|
|
this._descriptionElement.classList.add('monaco-button-description');
|
|
this._element.appendChild(this._descriptionElement);
|
|
|
|
container.appendChild(this._element);
|
|
}
|
|
|
|
get onDidClick(): BaseEvent<Event | undefined> {
|
|
return this._button.onDidClick;
|
|
}
|
|
|
|
get element(): HTMLElement {
|
|
return this._element;
|
|
}
|
|
|
|
set label(value: string) {
|
|
this._button.label = value;
|
|
}
|
|
|
|
set icon(icon: ThemeIcon) {
|
|
this._button.icon = icon;
|
|
}
|
|
|
|
get enabled(): boolean {
|
|
return this._button.enabled;
|
|
}
|
|
|
|
set enabled(enabled: boolean) {
|
|
this._button.enabled = enabled;
|
|
}
|
|
|
|
focus(): void {
|
|
this._button.focus();
|
|
}
|
|
hasFocus(): boolean {
|
|
return this._button.hasFocus();
|
|
}
|
|
dispose(): void {
|
|
this._button.dispose();
|
|
}
|
|
|
|
set description(value: string) {
|
|
if (this.options.supportIcons) {
|
|
reset(this._descriptionElement, ...renderLabelWithIcons(value));
|
|
} else {
|
|
this._descriptionElement.textContent = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ButtonBar extends Disposable {
|
|
|
|
private _buttons: IButton[] = [];
|
|
|
|
constructor(private readonly container: HTMLElement) {
|
|
super();
|
|
}
|
|
|
|
get buttons(): IButton[] {
|
|
return this._buttons;
|
|
}
|
|
|
|
addButton(options: IButtonOptions): IButton {
|
|
const button = this._register(new Button(this.container, options));
|
|
this.pushButton(button);
|
|
return button;
|
|
}
|
|
|
|
addButtonWithDescription(options: IButtonOptions): IButtonWithDescription {
|
|
const button = this._register(new ButtonWithDescription(this.container, options));
|
|
this.pushButton(button);
|
|
return button;
|
|
}
|
|
|
|
addButtonWithDropdown(options: IButtonWithDropdownOptions): IButton {
|
|
const button = this._register(new ButtonWithDropdown(this.container, options));
|
|
this.pushButton(button);
|
|
return button;
|
|
}
|
|
|
|
private pushButton(button: IButton): void {
|
|
this._buttons.push(button);
|
|
|
|
const index = this._buttons.length - 1;
|
|
this._register(addDisposableListener(button.element, EventType.KEY_DOWN, e => {
|
|
const event = new StandardKeyboardEvent(e);
|
|
let eventHandled = true;
|
|
|
|
// Next / Previous Button
|
|
let buttonIndexToFocus: number | undefined;
|
|
if (event.equals(KeyCode.LeftArrow)) {
|
|
buttonIndexToFocus = index > 0 ? index - 1 : this._buttons.length - 1;
|
|
} else if (event.equals(KeyCode.RightArrow)) {
|
|
buttonIndexToFocus = index === this._buttons.length - 1 ? 0 : index + 1;
|
|
} else {
|
|
eventHandled = false;
|
|
}
|
|
|
|
if (eventHandled && typeof buttonIndexToFocus === 'number') {
|
|
this._buttons[buttonIndexToFocus].focus();
|
|
EventHelper.stop(e, true);
|
|
}
|
|
|
|
}));
|
|
}
|
|
}
|