actionbar.ts 20.7 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!./actionbar';
7
import * as platform from 'vs/base/common/platform';
8
import * as nls from 'vs/nls';
9
import { Disposable, dispose } from 'vs/base/common/lifecycle';
10
import { SelectBox, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox';
I
isidor 已提交
11
import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, IRunEvent } from 'vs/base/common/actions';
12 13
import * as DOM from 'vs/base/browser/dom';
import * as types from 'vs/base/common/types';
14
import { EventType, Gesture } from 'vs/base/browser/touch';
J
Johannes Rieken 已提交
15
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
16
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
17
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
M
Matt Bierner 已提交
18
import { Event, Emitter } from 'vs/base/common/event';
E
Erich Gamma 已提交
19

I
isidor 已提交
20
export interface IActionItem {
M
Matt Bierner 已提交
21
	actionRunner: IActionRunner;
B
Benjamin Pasero 已提交
22 23 24
	setActionContext(context: any): void;
	render(element: HTMLElement): void;
	isEnabled(): boolean;
I
isidor 已提交
25
	focus(fromRight?: boolean): void;
B
Benjamin Pasero 已提交
26 27
	blur(): void;
	dispose(): void;
E
Erich Gamma 已提交
28 29
}

30 31
export interface IBaseActionItemOptions {
	draggable?: boolean;
32
	isMenu?: boolean;
33 34
}

35
export class BaseActionItem extends Disposable implements IActionItem {
E
Erich Gamma 已提交
36

M
Matt Bierner 已提交
37
	element?: HTMLElement;
38 39
	_context: any;
	_action: IAction;
B
Benjamin Pasero 已提交
40 41

	private _actionRunner: IActionRunner;
E
Erich Gamma 已提交
42

43
	constructor(context: any, action: IAction, protected options?: IBaseActionItemOptions) {
B
Benjamin Pasero 已提交
44 45
		super();

E
Erich Gamma 已提交
46 47 48
		this._context = context || this;
		this._action = action;

B
Benjamin Pasero 已提交
49
		if (action instanceof Action) {
B
Benjamin Pasero 已提交
50 51
			this._register(action.onDidChange(event => {
				if (!this.element) {
E
Erich Gamma 已提交
52 53 54 55
					// we have not been rendered yet, so there
					// is no point in updating the UI
					return;
				}
56 57

				this.handleActionChangeEvent(event);
58 59 60
			}));
		}
	}
E
Erich Gamma 已提交
61

62
	private handleActionChangeEvent(event: IActionChangeEvent): void {
63
		if (event.enabled !== void 0) {
64
			this.updateEnabled();
65
		}
66

67
		if (event.checked !== void 0) {
68
			this.updateChecked();
69
		}
70

71
		if (event.class !== void 0) {
72
			this.updateClass();
73
		}
74

75
		if (event.label !== void 0) {
76 77
			this.updateLabel();
			this.updateTooltip();
78
		}
79

80
		if (event.tooltip !== void 0) {
81
			this.updateTooltip();
E
Erich Gamma 已提交
82 83 84
		}
	}

85
	set actionRunner(actionRunner: IActionRunner) {
E
Erich Gamma 已提交
86 87 88
		this._actionRunner = actionRunner;
	}

89
	get actionRunner(): IActionRunner {
E
Erich Gamma 已提交
90 91 92
		return this._actionRunner;
	}

93
	getAction(): IAction {
E
Erich Gamma 已提交
94 95 96
		return this._action;
	}

97
	isEnabled(): boolean {
E
Erich Gamma 已提交
98 99 100
		return this._action.enabled;
	}

101
	setActionContext(newContext: any): void {
E
Erich Gamma 已提交
102 103 104
		this._context = newContext;
	}

105
	render(container: HTMLElement): void {
B
Benjamin Pasero 已提交
106
		this.element = container;
107
		Gesture.addTarget(container);
E
Erich Gamma 已提交
108

109 110 111 112 113
		const enableDragging = this.options && this.options.draggable;
		if (enableDragging) {
			container.draggable = true;
		}

B
Benjamin Pasero 已提交
114
		this._register(DOM.addDisposableListener(this.element, EventType.Tap, e => this.onClick(e)));
E
Erich Gamma 已提交
115

B
Benjamin Pasero 已提交
116
		this._register(DOM.addDisposableListener(this.element, DOM.EventType.MOUSE_DOWN, e => {
117
			if (!enableDragging) {
J
Joao Moreno 已提交
118
				DOM.EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
119 120
			}

M
Matt Bierner 已提交
121
			if (this._action.enabled && e.button === 0 && this.element) {
B
Benjamin Pasero 已提交
122
				DOM.addClass(this.element, 'active');
E
Erich Gamma 已提交
123
			}
B
Benjamin Pasero 已提交
124
		}));
125

B
Benjamin Pasero 已提交
126
		this._register(DOM.addDisposableListener(this.element, DOM.EventType.CLICK, e => {
J
Johannes Rieken 已提交
127
			DOM.EventHelper.stop(e, true);
128 129 130 131 132 133 134 135 136 137 138
			// See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
			// > Writing to the clipboard
			// > You can use the "cut" and "copy" commands without any special
			// permission if you are using them in a short-lived event handler
			// for a user action (for example, a click handler).

			// => to get the Copy and Paste context menu actions working on Firefox,
			// there should be no timeout here
			if (this.options && this.options.isMenu) {
				this.onClick(e);
			} else {
139
				platform.setImmediate(() => this.onClick(e));
140
			}
B
Benjamin Pasero 已提交
141
		}));
E
Erich Gamma 已提交
142

J
Joao Moreno 已提交
143 144 145 146
		this._register(DOM.addDisposableListener(this.element, DOM.EventType.DBLCLICK, e => {
			DOM.EventHelper.stop(e, true);
		}));

B
Benjamin Pasero 已提交
147
		[DOM.EventType.MOUSE_UP, DOM.EventType.MOUSE_OUT].forEach(event => {
M
Matt Bierner 已提交
148
			this._register(DOM.addDisposableListener(this.element!, event, e => {
B
Benjamin Pasero 已提交
149
				DOM.EventHelper.stop(e);
M
Matt Bierner 已提交
150
				DOM.removeClass(this.element!, 'active');
B
Benjamin Pasero 已提交
151
			}));
E
Erich Gamma 已提交
152 153 154
		});
	}

155
	onClick(event: DOM.EventLike): void {
B
Benjamin Pasero 已提交
156
		DOM.EventHelper.stop(event, true);
J
Joao Moreno 已提交
157

158
		let context: any;
S
SteVen Batten 已提交
159
		if (types.isUndefinedOrNull(this._context)) {
160 161 162
			context = event;
		} else {
			context = this._context;
S
SteVen Batten 已提交
163 164 165 166

			if (types.isObject(context)) {
				context.event = event;
			}
167 168
		}

169
		this._actionRunner.run(this._action, context);
E
Erich Gamma 已提交
170 171
	}

172
	focus(): void {
B
Benjamin Pasero 已提交
173 174 175
		if (this.element) {
			this.element.focus();
			DOM.addClass(this.element, 'focused');
176
		}
E
Erich Gamma 已提交
177 178
	}

179
	blur(): void {
B
Benjamin Pasero 已提交
180 181 182
		if (this.element) {
			this.element.blur();
			DOM.removeClass(this.element, 'focused');
183
		}
E
Erich Gamma 已提交
184 185
	}

186
	protected updateEnabled(): void {
E
Erich Gamma 已提交
187 188 189
		// implement in subclass
	}

190
	protected updateLabel(): void {
E
Erich Gamma 已提交
191 192 193
		// implement in subclass
	}

194
	protected updateTooltip(): void {
E
Erich Gamma 已提交
195 196 197
		// implement in subclass
	}

198
	protected updateClass(): void {
E
Erich Gamma 已提交
199 200 201
		// implement in subclass
	}

202
	protected updateChecked(): void {
E
Erich Gamma 已提交
203 204 205
		// implement in subclass
	}

206
	dispose(): void {
B
Benjamin Pasero 已提交
207
		if (this.element) {
A
Alex Dima 已提交
208
			DOM.removeNode(this.element);
M
Matt Bierner 已提交
209
			this.element = undefined;
E
Erich Gamma 已提交
210 211
		}

B
Benjamin Pasero 已提交
212
		super.dispose();
E
Erich Gamma 已提交
213 214 215
	}
}

B
Benjamin Pasero 已提交
216
export class Separator extends Action {
E
Erich Gamma 已提交
217

218
	static readonly ID = 'vs.actions.separator';
E
Erich Gamma 已提交
219

B
Benjamin Pasero 已提交
220
	constructor(label?: string) {
E
Erich Gamma 已提交
221 222
		super(Separator.ID, label, label ? 'separator text' : 'separator');
		this.checked = false;
223
		this.radio = false;
E
Erich Gamma 已提交
224 225 226 227
		this.enabled = false;
	}
}

228
export interface IActionItemOptions extends IBaseActionItemOptions {
B
Benjamin Pasero 已提交
229 230 231
	icon?: boolean;
	label?: boolean;
	keybinding?: string;
E
Erich Gamma 已提交
232 233 234 235
}

export class ActionItem extends BaseActionItem {

B
Benjamin Pasero 已提交
236
	protected label: HTMLElement;
J
Johannes Rieken 已提交
237
	protected options: IActionItemOptions;
B
Benjamin Pasero 已提交
238

M
Matt Bierner 已提交
239
	private cssClass?: string;
E
Erich Gamma 已提交
240

B
Benjamin Pasero 已提交
241
	constructor(context: any, action: IAction, options: IActionItemOptions = {}) {
242
		super(context, action, options);
E
Erich Gamma 已提交
243 244 245 246 247 248 249

		this.options = options;
		this.options.icon = options.icon !== undefined ? options.icon : false;
		this.options.label = options.label !== undefined ? options.label : true;
		this.cssClass = '';
	}

250
	render(container: HTMLElement): void {
E
Erich Gamma 已提交
251 252
		super.render(container);

M
Matt Bierner 已提交
253 254 255
		if (this.element) {
			this.label = DOM.append(this.element, DOM.$('a.action-label'));
		}
256
		if (this._action.id === Separator.ID) {
257
			this.label.setAttribute('role', 'presentation'); // A separator is a presentation item
A
Alex Dima 已提交
258
		} else {
259
			if (this.options.isMenu) {
B
Benjamin Pasero 已提交
260
				this.label.setAttribute('role', 'menuitem');
261
			} else {
B
Benjamin Pasero 已提交
262
				this.label.setAttribute('role', 'button');
263
			}
A
Alex Dima 已提交
264
		}
E
Erich Gamma 已提交
265

M
Matt Bierner 已提交
266
		if (this.options.label && this.options.keybinding && this.element) {
B
Benjamin Pasero 已提交
267
			DOM.append(this.element, DOM.$('span.keybinding')).textContent = this.options.keybinding;
E
Erich Gamma 已提交
268 269
		}

270 271 272 273 274
		this.updateClass();
		this.updateLabel();
		this.updateTooltip();
		this.updateEnabled();
		this.updateChecked();
E
Erich Gamma 已提交
275 276
	}

277
	focus(): void {
E
Erich Gamma 已提交
278
		super.focus();
279

B
Benjamin Pasero 已提交
280
		this.label.focus();
E
Erich Gamma 已提交
281 282
	}

283
	updateLabel(): void {
E
Erich Gamma 已提交
284
		if (this.options.label) {
B
Benjamin Pasero 已提交
285
			this.label.textContent = this.getAction().label;
E
Erich Gamma 已提交
286 287 288
		}
	}

289
	updateTooltip(): void {
290
		let title: string | null = null;
E
Erich Gamma 已提交
291 292 293 294 295 296 297 298

		if (this.getAction().tooltip) {
			title = this.getAction().tooltip;

		} else if (!this.options.label && this.getAction().label && this.options.icon) {
			title = this.getAction().label;

			if (this.options.keybinding) {
B
Benjamin Pasero 已提交
299
				title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
E
Erich Gamma 已提交
300 301 302 303
			}
		}

		if (title) {
B
Benjamin Pasero 已提交
304
			this.label.title = title;
E
Erich Gamma 已提交
305 306 307
		}
	}

308
	updateClass(): void {
E
Erich Gamma 已提交
309
		if (this.cssClass) {
B
Benjamin Pasero 已提交
310
			DOM.removeClasses(this.label, this.cssClass);
E
Erich Gamma 已提交
311
		}
312

E
Erich Gamma 已提交
313 314
		if (this.options.icon) {
			this.cssClass = this.getAction().class;
B
Benjamin Pasero 已提交
315
			DOM.addClass(this.label, 'icon');
E
Erich Gamma 已提交
316
			if (this.cssClass) {
B
Benjamin Pasero 已提交
317
				DOM.addClasses(this.label, this.cssClass);
E
Erich Gamma 已提交
318
			}
319 320

			this.updateEnabled();
E
Erich Gamma 已提交
321
		} else {
B
Benjamin Pasero 已提交
322
			DOM.removeClass(this.label, 'icon');
E
Erich Gamma 已提交
323 324 325
		}
	}

326
	updateEnabled(): void {
B
Benjamin Pasero 已提交
327
		if (this.getAction().enabled) {
328
			this.label.removeAttribute('aria-disabled');
M
Matt Bierner 已提交
329 330 331
			if (this.element) {
				DOM.removeClass(this.element, 'disabled');
			}
B
Benjamin Pasero 已提交
332 333
			DOM.removeClass(this.label, 'disabled');
			this.label.tabIndex = 0;
E
Erich Gamma 已提交
334
		} else {
335
			this.label.setAttribute('aria-disabled', 'true');
M
Matt Bierner 已提交
336 337 338
			if (this.element) {
				DOM.addClass(this.element, 'disabled');
			}
B
Benjamin Pasero 已提交
339 340
			DOM.addClass(this.label, 'disabled');
			DOM.removeTabIndexAndUpdateFocus(this.label);
E
Erich Gamma 已提交
341 342 343
		}
	}

344
	updateChecked(): void {
B
Benjamin Pasero 已提交
345
		if (this.getAction().checked) {
B
Benjamin Pasero 已提交
346
			DOM.addClass(this.label, 'checked');
E
Erich Gamma 已提交
347
		} else {
B
Benjamin Pasero 已提交
348
			DOM.removeClass(this.label, 'checked');
E
Erich Gamma 已提交
349 350 351 352
		}
	}
}

353
export const enum ActionsOrientation {
J
Joao Moreno 已提交
354 355 356 357
	HORIZONTAL,
	HORIZONTAL_REVERSE,
	VERTICAL,
	VERTICAL_REVERSE,
E
Erich Gamma 已提交
358 359
}

360 361 362 363 364
export interface ActionTrigger {
	keys: KeyCode[];
	keyDown: boolean;
}

E
Erich Gamma 已提交
365
export interface IActionItemProvider {
B
Benjamin Pasero 已提交
366
	(action: IAction): IActionItem;
E
Erich Gamma 已提交
367 368 369
}

export interface IActionBarOptions {
B
Benjamin Pasero 已提交
370 371 372
	orientation?: ActionsOrientation;
	context?: any;
	actionItemProvider?: IActionItemProvider;
B
Benjamin Pasero 已提交
373
	actionRunner?: IActionRunner;
374
	ariaLabel?: string;
J
Joao Moreno 已提交
375
	animated?: boolean;
376
	triggerKeys?: ActionTrigger;
E
Erich Gamma 已提交
377 378
}

B
Benjamin Pasero 已提交
379
let defaultOptions: IActionBarOptions = {
E
Erich Gamma 已提交
380
	orientation: ActionsOrientation.HORIZONTAL,
381 382 383 384 385
	context: null,
	triggerKeys: {
		keys: [KeyCode.Enter, KeyCode.Space],
		keyDown: false
	}
E
Erich Gamma 已提交
386 387 388
};

export interface IActionOptions extends IActionItemOptions {
B
Benjamin Pasero 已提交
389
	index?: number;
E
Erich Gamma 已提交
390 391
}

392
export class ActionBar extends Disposable implements IActionRunner {
E
Erich Gamma 已提交
393

394
	options: IActionBarOptions;
395

M
Matt Bierner 已提交
396
	private _actionRunner: IActionRunner;
E
Erich Gamma 已提交
397 398 399
	private _context: any;

	// Items
400
	items: IActionItem[];
M
Matt Bierner 已提交
401
	protected focusedItem?: number;
402
	private focusTracker: DOM.IFocusTracker;
E
Erich Gamma 已提交
403 404

	// Elements
405
	domNode: HTMLElement;
S
SteVen Batten 已提交
406
	protected actionsList: HTMLElement;
E
Erich Gamma 已提交
407

408 409 410 411 412 413 414 415 416 417 418
	private _onDidBlur = this._register(new Emitter<void>());
	get onDidBlur(): Event<void> { return this._onDidBlur.event; }

	private _onDidCancel = this._register(new Emitter<void>());
	get onDidCancel(): Event<void> { return this._onDidCancel.event; }

	private _onDidRun = this._register(new Emitter<IRunEvent>());
	get onDidRun(): Event<IRunEvent> { return this._onDidRun.event; }

	private _onDidBeforeRun = this._register(new Emitter<IRunEvent>());
	get onDidBeforeRun(): Event<IRunEvent> { return this._onDidBeforeRun.event; }
I
isidor 已提交
419

420
	constructor(container: HTMLElement, options: IActionBarOptions = defaultOptions) {
B
Benjamin Pasero 已提交
421 422
		super();

E
Erich Gamma 已提交
423 424 425
		this.options = options;
		this._context = options.context;

S
SteVen Batten 已提交
426 427 428 429
		if (!this.options.triggerKeys) {
			this.options.triggerKeys = defaultOptions.triggerKeys;
		}

M
Matt Bierner 已提交
430 431 432
		if (this.options.actionRunner) {
			this._actionRunner = this.options.actionRunner;
		} else {
B
Benjamin Pasero 已提交
433
			this._actionRunner = new ActionRunner();
B
Benjamin Pasero 已提交
434
			this._register(this._actionRunner);
E
Erich Gamma 已提交
435 436
		}

B
Benjamin Pasero 已提交
437 438
		this._register(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
		this._register(this._actionRunner.onDidBeforeRun(e => this._onDidBeforeRun.fire(e)));
E
Erich Gamma 已提交
439 440 441 442 443 444 445

		this.items = [];
		this.focusedItem = undefined;

		this.domNode = document.createElement('div');
		this.domNode.className = 'monaco-action-bar';

J
Joao Moreno 已提交
446 447 448 449
		if (options.animated !== false) {
			DOM.addClass(this.domNode, 'animated');
		}

J
Joao Moreno 已提交
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
		let previousKey: KeyCode;
		let nextKey: KeyCode;

		switch (this.options.orientation) {
			case ActionsOrientation.HORIZONTAL:
				previousKey = KeyCode.LeftArrow;
				nextKey = KeyCode.RightArrow;
				break;
			case ActionsOrientation.HORIZONTAL_REVERSE:
				previousKey = KeyCode.RightArrow;
				nextKey = KeyCode.LeftArrow;
				this.domNode.className += ' reverse';
				break;
			case ActionsOrientation.VERTICAL:
				previousKey = KeyCode.UpArrow;
				nextKey = KeyCode.DownArrow;
				this.domNode.className += ' vertical';
				break;
			case ActionsOrientation.VERTICAL_REVERSE:
				previousKey = KeyCode.DownArrow;
				nextKey = KeyCode.UpArrow;
				this.domNode.className += ' vertical reverse';
				break;
E
Erich Gamma 已提交
473 474
		}

B
Benjamin Pasero 已提交
475
		this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_DOWN, e => {
476
			let event = new StandardKeyboardEvent(e);
B
Benjamin Pasero 已提交
477
			let eventHandled = true;
E
Erich Gamma 已提交
478

J
Joao Moreno 已提交
479
			if (event.equals(previousKey)) {
E
Erich Gamma 已提交
480
				this.focusPrevious();
J
Joao Moreno 已提交
481
			} else if (event.equals(nextKey)) {
E
Erich Gamma 已提交
482
				this.focusNext();
A
Alexandru Dima 已提交
483
			} else if (event.equals(KeyCode.Escape)) {
E
Erich Gamma 已提交
484
				this.cancel();
485 486 487 488 489
			} else if (this.isTriggerKeyEvent(event)) {
				// Staying out of the else branch even if not triggered
				if (this.options.triggerKeys && this.options.triggerKeys.keyDown) {
					this.doTrigger(event);
				}
E
Erich Gamma 已提交
490 491 492 493
			} else {
				eventHandled = false;
			}

B
Benjamin Pasero 已提交
494
			if (eventHandled) {
E
Erich Gamma 已提交
495 496 497
				event.preventDefault();
				event.stopPropagation();
			}
B
Benjamin Pasero 已提交
498
		}));
E
Erich Gamma 已提交
499

B
Benjamin Pasero 已提交
500
		this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_UP, e => {
501
			let event = new StandardKeyboardEvent(e);
E
Erich Gamma 已提交
502

503
			// Run action on Enter/Space
504
			if (this.isTriggerKeyEvent(event)) {
M
Matt Bierner 已提交
505
				if (this.options.triggerKeys && !this.options.triggerKeys.keyDown) {
506 507 508
					this.doTrigger(event);
				}

E
Erich Gamma 已提交
509 510 511
				event.preventDefault();
				event.stopPropagation();
			}
512 513

			// Recompute focused item
A
Alexandru Dima 已提交
514
			else if (event.equals(KeyCode.Tab) || event.equals(KeyMod.Shift | KeyCode.Tab)) {
515 516
				this.updateFocusedItem();
			}
B
Benjamin Pasero 已提交
517
		}));
E
Erich Gamma 已提交
518

B
Benjamin Pasero 已提交
519 520
		this.focusTracker = this._register(DOM.trackFocus(this.domNode));
		this._register(this.focusTracker.onDidBlur(() => {
B
Benjamin Pasero 已提交
521
			if (document.activeElement === this.domNode || !DOM.isAncestor(document.activeElement, this.domNode)) {
I
isidor 已提交
522
				this._onDidBlur.fire();
523 524
				this.focusedItem = undefined;
			}
525
		}));
526

B
Benjamin Pasero 已提交
527
		this._register(this.focusTracker.onDidFocus(() => this.updateFocusedItem()));
E
Erich Gamma 已提交
528 529 530

		this.actionsList = document.createElement('ul');
		this.actionsList.className = 'actions-container';
S
SteVen Batten 已提交
531 532
		this.actionsList.setAttribute('role', 'toolbar');

533 534 535 536
		if (this.options.ariaLabel) {
			this.actionsList.setAttribute('aria-label', this.options.ariaLabel);
		}

E
Erich Gamma 已提交
537 538
		this.domNode.appendChild(this.actionsList);

539
		container.appendChild(this.domNode);
E
Erich Gamma 已提交
540 541
	}

542
	setAriaLabel(label: string): void {
543 544
		if (label) {
			this.actionsList.setAttribute('aria-label', label);
B
Benjamin Pasero 已提交
545
		} else {
546 547 548 549
			this.actionsList.removeAttribute('aria-label');
		}
	}

550 551 552 553 554 555 556 557 558 559 560
	private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean {
		let ret = false;
		if (this.options.triggerKeys) {
			this.options.triggerKeys.keys.forEach(keyCode => {
				ret = ret || event.equals(keyCode);
			});
		}

		return ret;
	}

561 562 563 564 565 566 567 568 569 570
	private updateFocusedItem(): void {
		for (let i = 0; i < this.actionsList.children.length; i++) {
			let elem = this.actionsList.children[i];
			if (DOM.isAncestor(document.activeElement, elem)) {
				this.focusedItem = i;
				break;
			}
		}
	}

571
	get context(): any {
E
Erich Gamma 已提交
572 573 574
		return this._context;
	}

575
	set context(context: any) {
E
Erich Gamma 已提交
576 577 578 579
		this._context = context;
		this.items.forEach(i => i.setActionContext(context));
	}

M
Matt Bierner 已提交
580
	get actionRunner(): IActionRunner | undefined {
E
Erich Gamma 已提交
581 582 583
		return this._actionRunner;
	}

M
Matt Bierner 已提交
584
	set actionRunner(actionRunner: IActionRunner | undefined) {
E
Erich Gamma 已提交
585 586 587 588 589 590
		if (actionRunner) {
			this._actionRunner = actionRunner;
			this.items.forEach(item => item.actionRunner = actionRunner);
		}
	}

591
	getContainer(): HTMLElement {
592
		return this.domNode;
E
Erich Gamma 已提交
593 594
	}

595
	push(arg: IAction | IAction[], options: IActionOptions = {}): void {
596
		const actions: IAction[] = !Array.isArray(arg) ? [arg] : arg;
E
Erich Gamma 已提交
597

B
Benjamin Pasero 已提交
598
		let index = types.isNumber(options.index) ? options.index : null;
E
Erich Gamma 已提交
599

B
Benjamin Pasero 已提交
600
		actions.forEach((action: IAction) => {
601
			const actionItemElement = document.createElement('li');
E
Erich Gamma 已提交
602 603 604
			actionItemElement.className = 'action-item';
			actionItemElement.setAttribute('role', 'presentation');

605
			// Prevent native context menu on actions
B
Benjamin Pasero 已提交
606
			this._register(DOM.addDisposableListener(actionItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => {
607 608
				e.preventDefault();
				e.stopPropagation();
B
Benjamin Pasero 已提交
609
			}));
610

611
			let item: IActionItem | null = null;
E
Erich Gamma 已提交
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626

			if (this.options.actionItemProvider) {
				item = this.options.actionItemProvider(action);
			}

			if (!item) {
				item = new ActionItem(this.context, action, options);
			}

			item.actionRunner = this._actionRunner;
			item.setActionContext(this.context);
			item.render(actionItemElement);

			if (index === null || index < 0 || index >= this.actionsList.children.length) {
				this.actionsList.appendChild(actionItemElement);
627
				this.items.push(item);
E
Erich Gamma 已提交
628
			} else {
629 630 631
				this.actionsList.insertBefore(actionItemElement, this.actionsList.children[index]);
				this.items.splice(index, 0, item);
				index++;
E
Erich Gamma 已提交
632 633 634 635
			}
		});
	}

636
	getWidth(index: number): number {
637
		if (index >= 0 && index < this.actionsList.children.length) {
M
Matt Bierner 已提交
638 639 640 641
			const item = this.actionsList.children.item(index);
			if (item) {
				return item.clientWidth;
			}
642 643 644 645 646
		}

		return 0;
	}

647
	getHeight(index: number): number {
648
		if (index >= 0 && index < this.actionsList.children.length) {
M
Matt Bierner 已提交
649 650 651 652
			const item = this.actionsList.children.item(index);
			if (item) {
				return item.clientHeight;
			}
653 654 655 656 657
		}

		return 0;
	}

658
	pull(index: number): void {
P
Pine Wu 已提交
659
		if (index >= 0 && index < this.items.length) {
P
Pine Wu 已提交
660 661 662 663 664
			this.items.splice(index, 1);
			this.actionsList.removeChild(this.actionsList.childNodes[index]);
		}
	}

665 666
	clear(): void {
		this.items = dispose(this.items);
B
Benjamin Pasero 已提交
667
		DOM.clearNode(this.actionsList);
E
Erich Gamma 已提交
668 669
	}

670
	length(): number {
E
Erich Gamma 已提交
671 672 673
		return this.items.length;
	}

674
	isEmpty(): boolean {
E
Erich Gamma 已提交
675 676 677
		return this.items.length === 0;
	}

678
	focus(selectFirst?: boolean): void {
E
Erich Gamma 已提交
679
		if (selectFirst && typeof this.focusedItem === 'undefined') {
S
SteVen Batten 已提交
680 681 682 683 684
			// Focus the first enabled item
			this.focusedItem = this.items.length - 1;
			this.focusNext();
		} else {
			this.updateFocus();
E
Erich Gamma 已提交
685 686 687
		}
	}

B
Benjamin Pasero 已提交
688
	private focusNext(): void {
E
Erich Gamma 已提交
689 690 691 692
		if (typeof this.focusedItem === 'undefined') {
			this.focusedItem = this.items.length - 1;
		}

B
Benjamin Pasero 已提交
693 694
		let startIndex = this.focusedItem;
		let item: IActionItem;
E
Erich Gamma 已提交
695 696 697 698 699 700 701 702 703 704 705 706 707

		do {
			this.focusedItem = (this.focusedItem + 1) % this.items.length;
			item = this.items[this.focusedItem];
		} while (this.focusedItem !== startIndex && !item.isEnabled());

		if (this.focusedItem === startIndex && !item.isEnabled()) {
			this.focusedItem = undefined;
		}

		this.updateFocus();
	}

B
Benjamin Pasero 已提交
708
	private focusPrevious(): void {
E
Erich Gamma 已提交
709 710 711 712
		if (typeof this.focusedItem === 'undefined') {
			this.focusedItem = 0;
		}

B
Benjamin Pasero 已提交
713 714
		let startIndex = this.focusedItem;
		let item: IActionItem;
E
Erich Gamma 已提交
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729

		do {
			this.focusedItem = this.focusedItem - 1;

			if (this.focusedItem < 0) {
				this.focusedItem = this.items.length - 1;
			}

			item = this.items[this.focusedItem];
		} while (this.focusedItem !== startIndex && !item.isEnabled());

		if (this.focusedItem === startIndex && !item.isEnabled()) {
			this.focusedItem = undefined;
		}

I
isidor 已提交
730
		this.updateFocus(true);
E
Erich Gamma 已提交
731 732
	}

S
SteVen Batten 已提交
733
	protected updateFocus(fromRight?: boolean): void {
E
Erich Gamma 已提交
734
		if (typeof this.focusedItem === 'undefined') {
735
			this.actionsList.focus();
E
Erich Gamma 已提交
736 737
		}

B
Benjamin Pasero 已提交
738 739
		for (let i = 0; i < this.items.length; i++) {
			let item = this.items[i];
E
Erich Gamma 已提交
740

741
			let actionItem = item;
E
Erich Gamma 已提交
742

B
Benjamin Pasero 已提交
743
			if (i === this.focusedItem) {
744 745 746 747
				if (types.isFunction(actionItem.isEnabled)) {
					if (actionItem.isEnabled() && types.isFunction(actionItem.focus)) {
						actionItem.focus(fromRight);
					} else {
748
						this.actionsList.focus();
749
					}
E
Erich Gamma 已提交
750 751
				}
			} else {
B
Benjamin Pasero 已提交
752
				if (types.isFunction(actionItem.blur)) {
E
Erich Gamma 已提交
753 754 755 756 757 758
					actionItem.blur();
				}
			}
		}
	}

B
Benjamin Pasero 已提交
759
	private doTrigger(event: StandardKeyboardEvent): void {
B
Benjamin Pasero 已提交
760
		if (typeof this.focusedItem === 'undefined') {
761
			return; //nothing to focus
E
Erich Gamma 已提交
762 763 764
		}

		// trigger action
I
isidor 已提交
765 766 767
		let actionItem = this.items[this.focusedItem];
		if (actionItem instanceof BaseActionItem) {
			const context = (actionItem._context === null || actionItem._context === undefined) ? event : actionItem._context;
768
			this.run(actionItem._action, context);
I
isidor 已提交
769
		}
E
Erich Gamma 已提交
770 771
	}

B
Benjamin Pasero 已提交
772
	private cancel(): void {
B
Benjamin Pasero 已提交
773
		if (document.activeElement instanceof HTMLElement) {
774
			(<HTMLElement>document.activeElement).blur(); // remove focus from focused action
B
Benjamin Pasero 已提交
775 776
		}

I
isidor 已提交
777
		this._onDidCancel.fire();
E
Erich Gamma 已提交
778 779
	}

780
	run(action: IAction, context?: any): Thenable<void> {
M
Matt Bierner 已提交
781
		return this._actionRunner.run(action, context);
E
Erich Gamma 已提交
782 783
	}

784
	dispose(): void {
M
Matt Bierner 已提交
785 786
		dispose(this.items);
		this.items = [];
E
Erich Gamma 已提交
787

A
Alex Dima 已提交
788
		DOM.removeNode(this.getContainer());
B
Benjamin Pasero 已提交
789 790

		super.dispose();
E
Erich Gamma 已提交
791 792 793 794
	}
}

export class SelectActionItem extends BaseActionItem {
795
	protected selectBox: SelectBox;
E
Erich Gamma 已提交
796

797
	constructor(ctx: any, action: IAction, options: string[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) {
E
Erich Gamma 已提交
798
		super(ctx, action);
B
Benjamin Pasero 已提交
799

M
Matt Bierner 已提交
800
		this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions);
E
Erich Gamma 已提交
801

B
Benjamin Pasero 已提交
802
		this._register(this.selectBox);
E
Erich Gamma 已提交
803 804 805
		this.registerListeners();
	}

806
	setOptions(options: string[], selected?: number, disabled?: number): void {
S
Sandeep Somavarapu 已提交
807
		this.selectBox.setOptions(options, selected, disabled);
E
Erich Gamma 已提交
808 809
	}

810
	select(index: number): void {
I
isidor 已提交
811 812 813
		this.selectBox.select(index);
	}

E
Erich Gamma 已提交
814
	private registerListeners(): void {
B
Benjamin Pasero 已提交
815
		this._register(this.selectBox.onDidSelect(e => {
S
Sandeep Somavarapu 已提交
816
			this.actionRunner.run(this._action, this.getActionContext(e.selected, e.index));
E
Erich Gamma 已提交
817 818 819
		}));
	}

S
Sandeep Somavarapu 已提交
820
	protected getActionContext(option: string, index: number) {
I
isidor 已提交
821 822 823
		return option;
	}

824
	focus(): void {
I
isidor 已提交
825 826
		if (this.selectBox) {
			this.selectBox.focus();
827 828 829
		}
	}

830
	blur(): void {
I
isidor 已提交
831 832
		if (this.selectBox) {
			this.selectBox.blur();
833 834 835
		}
	}

836
	render(container: HTMLElement): void {
I
isidor 已提交
837
		this.selectBox.render(container);
E
Erich Gamma 已提交
838
	}
839
}