button.ts 10.8 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import 'vs/css!./button';
J
Johannes Rieken 已提交
7 8
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
9 10
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
J
Joao Moreno 已提交
11
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
12
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
J
João Moreno 已提交
13
import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch';
14
import { renderCodicons } from 'vs/base/browser/codicons';
J
João Moreno 已提交
15
import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
S
Sandeep Somavarapu 已提交
16 17
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
import { IAction } from 'vs/base/common/actions';
B
Benjamin Pasero 已提交
18 19

export interface IButtonOptions extends IButtonStyles {
20 21
	readonly title?: boolean | string;
	readonly supportCodicons?: boolean;
22
	readonly secondary?: boolean;
B
Benjamin Pasero 已提交
23 24 25 26 27 28
}

export interface IButtonStyles {
	buttonBackground?: Color;
	buttonHoverBackground?: Color;
	buttonForeground?: Color;
29 30 31
	buttonSecondaryBackground?: Color;
	buttonSecondaryHoverBackground?: Color;
	buttonSecondaryForeground?: Color;
B
Benjamin Pasero 已提交
32
	buttonBorder?: Color;
B
Benjamin Pasero 已提交
33 34 35 36 37 38 39
}

const defaultOptions: IButtonStyles = {
	buttonBackground: Color.fromHex('#0E639C'),
	buttonHoverBackground: Color.fromHex('#006BB3'),
	buttonForeground: Color.white
};
E
Erich Gamma 已提交
40

41 42 43 44 45 46 47 48 49 50 51 52
export interface IButton extends IDisposable {
	readonly element: HTMLElement;
	readonly onDidClick: BaseEvent<Event>;
	label: string;
	icon: string;
	enabled: boolean;
	style(styles: IButtonStyles): void;
	focus(): void;
	hasFocus(): boolean;
}

export class Button extends Disposable implements IButton {
E
Erich Gamma 已提交
53

B
Benjamin Pasero 已提交
54
	private _element: HTMLElement;
B
Benjamin Pasero 已提交
55
	private options: IButtonOptions;
E
Erich Gamma 已提交
56

M
Matt Bierner 已提交
57 58 59
	private buttonBackground: Color | undefined;
	private buttonHoverBackground: Color | undefined;
	private buttonForeground: Color | undefined;
60 61 62
	private buttonSecondaryBackground: Color | undefined;
	private buttonSecondaryHoverBackground: Color | undefined;
	private buttonSecondaryForeground: Color | undefined;
M
Matt Bierner 已提交
63
	private buttonBorder: Color | undefined;
B
Benjamin Pasero 已提交
64

65
	private _onDidClick = this._register(new Emitter<Event>());
B
Benjamin Pasero 已提交
66
	get onDidClick(): BaseEvent<Event> { return this._onDidClick.event; }
J
Joao Moreno 已提交
67

J
João Moreno 已提交
68
	private focusTracker: IFocusTracker;
69

70
	constructor(container: HTMLElement, options?: IButtonOptions) {
B
Benjamin Pasero 已提交
71 72
		super();

B
Benjamin Pasero 已提交
73 74 75
		this.options = options || Object.create(null);
		mixin(this.options, defaultOptions, false);

76
		this.buttonForeground = this.options.buttonForeground;
B
Benjamin Pasero 已提交
77 78
		this.buttonBackground = this.options.buttonBackground;
		this.buttonHoverBackground = this.options.buttonHoverBackground;
79 80 81 82 83

		this.buttonSecondaryForeground = this.options.buttonSecondaryForeground;
		this.buttonSecondaryBackground = this.options.buttonSecondaryBackground;
		this.buttonSecondaryHoverBackground = this.options.buttonSecondaryHoverBackground;

B
Benjamin Pasero 已提交
84
		this.buttonBorder = this.options.buttonBorder;
B
Benjamin Pasero 已提交
85

B
Benjamin Pasero 已提交
86
		this._element = document.createElement('a');
J
João Moreno 已提交
87
		this._element.classList.add('monaco-button');
B
Benjamin Pasero 已提交
88 89
		this._element.tabIndex = 0;
		this._element.setAttribute('role', 'button');
E
Erich Gamma 已提交
90

B
Benjamin Pasero 已提交
91
		container.appendChild(this._element);
92

93
		this._register(Gesture.addTarget(this._element));
94

J
João Moreno 已提交
95 96
		[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {
			this._register(addDisposableListener(this._element, eventType, e => {
B
Benjamin Pasero 已提交
97
				if (!this.enabled) {
J
João Moreno 已提交
98
					EventHelper.stop(e);
B
Benjamin Pasero 已提交
99 100
					return;
				}
E
Erich Gamma 已提交
101

B
Benjamin Pasero 已提交
102 103 104
				this._onDidClick.fire(e);
			}));
		});
105

J
João Moreno 已提交
106
		this._register(addDisposableListener(this._element, EventType.KEY_DOWN, e => {
107
			const event = new StandardKeyboardEvent(e);
108
			let eventHandled = false;
K
Keyon You 已提交
109
			if (this.enabled && (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) {
J
Joao Moreno 已提交
110
				this._onDidClick.fire(e);
111
				eventHandled = true;
A
Alexandru Dima 已提交
112
			} else if (event.equals(KeyCode.Escape)) {
B
Benjamin Pasero 已提交
113
				this._element.blur();
114 115 116 117
				eventHandled = true;
			}

			if (eventHandled) {
J
João Moreno 已提交
118
				EventHelper.stop(event, true);
119
			}
120
		}));
B
Benjamin Pasero 已提交
121

J
João Moreno 已提交
122 123
		this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => {
			if (!this._element.classList.contains('disabled')) {
124
				this.setHoverBackground();
B
Benjamin Pasero 已提交
125
			}
126
		}));
B
Benjamin Pasero 已提交
127

J
João Moreno 已提交
128
		this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => {
B
Benjamin Pasero 已提交
129
			this.applyStyles(); // restore standard styles
130
		}));
B
Benjamin Pasero 已提交
131

132
		// Also set hover background when button is focused for feedback
J
João Moreno 已提交
133
		this.focusTracker = this._register(trackFocus(this._element));
B
Benjamin Pasero 已提交
134 135
		this._register(this.focusTracker.onDidFocus(() => this.setHoverBackground()));
		this._register(this.focusTracker.onDidBlur(() => this.applyStyles())); // restore standard styles
136

B
Benjamin Pasero 已提交
137 138 139
		this.applyStyles();
	}

140
	private setHoverBackground(): void {
141
		let hoverBackground;
142
		if (this.options.secondary) {
143 144 145 146
			hoverBackground = this.buttonSecondaryHoverBackground ? this.buttonSecondaryHoverBackground.toString() : null;
		} else {
			hoverBackground = this.buttonHoverBackground ? this.buttonHoverBackground.toString() : null;
		}
147
		if (hoverBackground) {
B
Benjamin Pasero 已提交
148
			this._element.style.backgroundColor = hoverBackground;
149 150 151
		}
	}

B
Benjamin Pasero 已提交
152 153 154 155
	style(styles: IButtonStyles): void {
		this.buttonForeground = styles.buttonForeground;
		this.buttonBackground = styles.buttonBackground;
		this.buttonHoverBackground = styles.buttonHoverBackground;
156 157 158
		this.buttonSecondaryForeground = styles.buttonSecondaryForeground;
		this.buttonSecondaryBackground = styles.buttonSecondaryBackground;
		this.buttonSecondaryHoverBackground = styles.buttonSecondaryHoverBackground;
B
Benjamin Pasero 已提交
159
		this.buttonBorder = styles.buttonBorder;
B
Benjamin Pasero 已提交
160 161 162 163 164

		this.applyStyles();
	}

	private applyStyles(): void {
B
Benjamin Pasero 已提交
165
		if (this._element) {
166
			let background, foreground;
167
			if (this.options.secondary) {
168 169 170 171 172 173 174
				foreground = this.buttonSecondaryForeground ? this.buttonSecondaryForeground.toString() : '';
				background = this.buttonSecondaryBackground ? this.buttonSecondaryBackground.toString() : '';
			} else {
				foreground = this.buttonForeground ? this.buttonForeground.toString() : '';
				background = this.buttonBackground ? this.buttonBackground.toString() : '';
			}

M
Matt Bierner 已提交
175
			const border = this.buttonBorder ? this.buttonBorder.toString() : '';
B
Benjamin Pasero 已提交
176

B
Benjamin Pasero 已提交
177 178
			this._element.style.color = foreground;
			this._element.style.backgroundColor = background;
B
Benjamin Pasero 已提交
179

M
Matt Bierner 已提交
180 181
			this._element.style.borderWidth = border ? '1px' : '';
			this._element.style.borderStyle = border ? 'solid' : '';
B
Benjamin Pasero 已提交
182
			this._element.style.borderColor = border;
B
Benjamin Pasero 已提交
183
		}
E
Erich Gamma 已提交
184 185
	}

186
	get element(): HTMLElement {
B
Benjamin Pasero 已提交
187
		return this._element;
E
Erich Gamma 已提交
188 189
	}

J
Joao Moreno 已提交
190
	set label(value: string) {
J
João Moreno 已提交
191
		this._element.classList.add('monaco-text-button');
192
		if (this.options.supportCodicons) {
J
João Moreno 已提交
193
			reset(this._element, ...renderCodicons(value));
194 195 196
		} else {
			this._element.textContent = value;
		}
197 198 199
		if (typeof this.options.title === 'string') {
			this._element.title = this.options.title;
		} else if (this.options.title) {
B
Benjamin Pasero 已提交
200
			this._element.title = value;
201
		}
E
Erich Gamma 已提交
202 203
	}

J
Joao Moreno 已提交
204
	set icon(iconClassName: string) {
205
		this._element.classList.add(...iconClassName.split(' '));
206 207
	}

J
Joao Moreno 已提交
208
	set enabled(value: boolean) {
E
Erich Gamma 已提交
209
		if (value) {
J
João Moreno 已提交
210
			this._element.classList.remove('disabled');
B
Benjamin Pasero 已提交
211 212
			this._element.setAttribute('aria-disabled', String(false));
			this._element.tabIndex = 0;
E
Erich Gamma 已提交
213
		} else {
J
João Moreno 已提交
214
			this._element.classList.add('disabled');
B
Benjamin Pasero 已提交
215
			this._element.setAttribute('aria-disabled', String(true));
J
João Moreno 已提交
216
			removeTabIndexAndUpdateFocus(this._element);
E
Erich Gamma 已提交
217 218 219
		}
	}

J
Joao Moreno 已提交
220
	get enabled() {
J
João Moreno 已提交
221
		return !this._element.classList.contains('disabled');
E
Erich Gamma 已提交
222 223
	}

J
Joao Moreno 已提交
224
	focus(): void {
B
Benjamin Pasero 已提交
225
		this._element.focus();
J
Joao Moreno 已提交
226
	}
227 228 229 230

	hasFocus(): boolean {
		return this._element === document.activeElement;
	}
231 232
}

233
export interface IButtonWithDropdownOptions extends IButtonOptions {
S
Sandeep Somavarapu 已提交
234 235
	readonly contextMenuProvider: IContextMenuProvider;
	readonly actions: IAction[];
236 237 238 239 240 241 242 243 244
}

export class ButtonWithDropdown extends Disposable implements IButton {

	private readonly button: Button;
	private readonly dropdownButton: Button;

	readonly element: HTMLElement;
	readonly onDidClick: BaseEvent<Event>;
245

246
	constructor(container: HTMLElement, options: IButtonWithDropdownOptions) {
B
Benjamin Pasero 已提交
247
		super();
248

249 250 251 252 253 254 255 256 257 258
		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.onDidClick = this.button.onDidClick;

		this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportCodicons: true }));
		this.dropdownButton.element.classList.add('monaco-dropdown-button');
		this.dropdownButton.icon = 'codicon codicon-chevron-down';
S
Sandeep Somavarapu 已提交
259 260 261 262 263 264 265 266
		this._register(this.dropdownButton.onDidClick(() => {
			options.contextMenuProvider.showContextMenu({
				getAnchor: () => this.dropdownButton.element,
				getActions: () => options.actions,
				onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false')
			});
			this.dropdownButton.element.setAttribute('aria-expanded', 'true');
		}));
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
	}

	set label(value: string) {
		this.button.label = value;
	}

	set icon(iconClassName: string) {
		this.button.icon = iconClassName;
	}

	set enabled(enabled: boolean) {
		this.button.enabled = enabled;
		this.dropdownButton.enabled = enabled;
	}

	get enabled(): boolean {
		return this.button.enabled;
	}

	style(styles: IButtonStyles): void {
		this.button.style(styles);
		this.dropdownButton.style(styles);
	}

	focus(): void {
		this.button.focus();
293 294
	}

295 296 297 298 299 300 301 302 303 304 305 306 307 308
	hasFocus(): boolean {
		return this.button.hasFocus() || this.dropdownButton.hasFocus();
	}
}

export class ButtonBar extends Disposable {

	private _buttons: IButton[] = [];

	constructor(private readonly container: HTMLElement) {
		super();
	}

	get buttons(): IButton[] {
309 310 311
		return this._buttons;
	}

312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
	addButton(options?: IButtonOptions): IButton {
		const button = this._register(new Button(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;
340
			}
341 342 343 344 345 346 347 348

			if (eventHandled && typeof buttonIndexToFocus === 'number') {
				this._buttons[buttonIndexToFocus].focus();
				EventHelper.stop(e, true);
			}

		}));

349
	}
350

P
Peng Lyu 已提交
351
}