listWidget.ts 52.2 KB
Newer Older
J
Joao Moreno 已提交
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!./list';
J
Joao Moreno 已提交
7
import { localize } from 'vs/nls';
M
Matt Bierner 已提交
8
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
9
import { isNumber } from 'vs/base/common/types';
J
Joao Moreno 已提交
10
import { range, firstIndex, binarySearch } from 'vs/base/common/arrays';
J
Joao Moreno 已提交
11
import { memoize } from 'vs/base/common/decorators';
J
Joao Moreno 已提交
12
import * as DOM from 'vs/base/browser/dom';
J
Joao Moreno 已提交
13
import * as platform from 'vs/base/common/platform';
J
Joao Moreno 已提交
14
import { Gesture } from 'vs/base/browser/touch';
15
import { KeyCode } from 'vs/base/common/keyCodes';
16
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
J
Joao Moreno 已提交
17
import { Event, Emitter, EventBufferer } from 'vs/base/common/event';
J
Joao Moreno 已提交
18
import { domEvent } from 'vs/base/browser/event';
J
João Moreno 已提交
19
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListError, IKeyboardNavigationDelegate } from './list';
J
João Moreno 已提交
20
import { ListView, IListViewOptions, IListViewDragAndDrop, IListViewAccessibilityProvider } from './listView';
21
import { Color } from 'vs/base/common/color';
J
Joao Moreno 已提交
22
import { mixin } from 'vs/base/common/objects';
J
Joao Moreno 已提交
23
import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
J
Joao Moreno 已提交
24
import { ISpliceable } from 'vs/base/common/sequence';
J
Joao Moreno 已提交
25
import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice';
J
Joao Moreno 已提交
26
import { clamp } from 'vs/base/common/numbers';
J
Joao Moreno 已提交
27
import { matchesPrefix } from 'vs/base/common/filters';
J
Joao Moreno 已提交
28
import { IDragAndDropData } from 'vs/base/browser/dnd';
29
import { alert } from 'vs/base/browser/ui/aria/aria';
J
Joao Moreno 已提交
30

J
Joao Moreno 已提交
31 32
interface ITraitChangeEvent {
	indexes: number[];
33
	browserEvent?: UIEvent;
J
Joao Moreno 已提交
34 35
}

J
Joao Moreno 已提交
36
type ITraitTemplateData = HTMLElement;
J
Joao Moreno 已提交
37

J
Joao Moreno 已提交
38
interface IRenderedContainer {
J
Joao Moreno 已提交
39 40
	templateData: ITraitTemplateData;
	index: number;
J
Joao Moreno 已提交
41 42
}

J
Joao Moreno 已提交
43
class TraitRenderer<T> implements IListRenderer<T, ITraitTemplateData>
J
Joao Moreno 已提交
44
{
J
Joao Moreno 已提交
45
	private renderedElements: IRenderedContainer[] = [];
J
Joao Moreno 已提交
46 47

	constructor(private trait: Trait<T>) { }
J
Joao Moreno 已提交
48

J
Joao Moreno 已提交
49
	get templateId(): string {
J
Joao Moreno 已提交
50 51 52 53
		return `template:${this.trait.trait}`;
	}

	renderTemplate(container: HTMLElement): ITraitTemplateData {
J
Joao Moreno 已提交
54
		return container;
J
Joao Moreno 已提交
55 56 57
	}

	renderElement(element: T, index: number, templateData: ITraitTemplateData): void {
J
Joao Moreno 已提交
58
		const renderedElementIndex = firstIndex(this.renderedElements, el => el.templateData === templateData);
J
Joao Moreno 已提交
59

J
Joao Moreno 已提交
60 61 62 63 64 65 66 67
		if (renderedElementIndex >= 0) {
			const rendered = this.renderedElements[renderedElementIndex];
			this.trait.unrender(templateData);
			rendered.index = index;
		} else {
			const rendered = { index, templateData };
			this.renderedElements.push(rendered);
		}
J
Joao Moreno 已提交
68

J
Joao Moreno 已提交
69
		this.trait.renderIndex(index, templateData);
J
Joao Moreno 已提交
70 71
	}

J
Joao Moreno 已提交
72 73
	splice(start: number, deleteCount: number, insertCount: number): void {
		const rendered: IRenderedContainer[] = [];
J
Joao Moreno 已提交
74

75
		for (const renderedElement of this.renderedElements) {
A
Alex Dima 已提交
76

J
Joao Moreno 已提交
77 78 79 80 81 82 83
			if (renderedElement.index < start) {
				rendered.push(renderedElement);
			} else if (renderedElement.index >= start + deleteCount) {
				rendered.push({
					index: renderedElement.index + insertCount - deleteCount,
					templateData: renderedElement.templateData
				});
J
Joao Moreno 已提交
84 85
			}
		}
J
Joao Moreno 已提交
86 87

		this.renderedElements = rendered;
J
Joao Moreno 已提交
88 89
	}

J
Joao Moreno 已提交
90 91 92 93
	renderIndexes(indexes: number[]): void {
		for (const { index, templateData } of this.renderedElements) {
			if (indexes.indexOf(index) > -1) {
				this.trait.renderIndex(index, templateData);
J
Joao Moreno 已提交
94 95
			}
		}
J
Joao Moreno 已提交
96 97
	}

J
Joao Moreno 已提交
98
	disposeTemplate(templateData: ITraitTemplateData): void {
J
Joao Moreno 已提交
99 100 101 102 103 104 105
		const index = firstIndex(this.renderedElements, el => el.templateData === templateData);

		if (index < 0) {
			return;
		}

		this.renderedElements.splice(index, 1);
J
Joao Moreno 已提交
106 107 108
	}
}

109
class Trait<T> implements ISpliceable<boolean>, IDisposable {
J
Joao Moreno 已提交
110

J
Joao Moreno 已提交
111 112
	private indexes: number[] = [];
	private sortedIndexes: number[] = [];
J
Joao Moreno 已提交
113

114
	private readonly _onChange = new Emitter<ITraitChangeEvent>();
115
	readonly onChange: Event<ITraitChangeEvent> = this._onChange.event;
J
Joao Moreno 已提交
116 117 118 119

	get trait(): string { return this._trait; }

	@memoize
M
Matt Bierner 已提交
120 121
	get renderer(): TraitRenderer<T> {
		return new TraitRenderer<T>(this);
J
Joao Moreno 已提交
122
	}
J
Joao Moreno 已提交
123

J
Joao Moreno 已提交
124
	constructor(private _trait: string) { }
J
Joao Moreno 已提交
125

J
Joao Moreno 已提交
126 127
	splice(start: number, deleteCount: number, elements: boolean[]): void {
		const diff = elements.length - deleteCount;
J
Joao Moreno 已提交
128
		const end = start + deleteCount;
J
Joao Moreno 已提交
129
		const indexes = [
J
Joao Moreno 已提交
130
			...this.sortedIndexes.filter(i => i < start),
131
			...elements.map((hasTrait, i) => hasTrait ? i + start : -1).filter(i => i !== -1),
J
Joao Moreno 已提交
132
			...this.sortedIndexes.filter(i => i >= end).map(i => i + diff)
J
Joao Moreno 已提交
133
		];
J
Joao Moreno 已提交
134

J
Joao Moreno 已提交
135
		this.renderer.splice(start, deleteCount, elements.length);
J
Joao Moreno 已提交
136
		this._set(indexes, indexes);
J
Joao Moreno 已提交
137 138
	}

J
Joao Moreno 已提交
139
	renderIndex(index: number, container: HTMLElement): void {
A
Alex Dima 已提交
140
		DOM.toggleClass(container, this._trait, this.contains(index));
J
Joao Moreno 已提交
141 142
	}

J
Joao Moreno 已提交
143 144 145 146
	unrender(container: HTMLElement): void {
		DOM.removeClass(container, this._trait);
	}

J
Joao Moreno 已提交
147 148 149 150 151 152
	/**
	 * Sets the indexes which should have this trait.
	 *
	 * @param indexes Indexes which should have this trait.
	 * @return The old indexes which had this trait.
	 */
153
	set(indexes: number[], browserEvent?: UIEvent): number[] {
J
Joao Moreno 已提交
154 155 156 157
		return this._set(indexes, [...indexes].sort(numericSort), browserEvent);
	}

	private _set(indexes: number[], sortedIndexes: number[], browserEvent?: UIEvent): number[] {
J
Joao Moreno 已提交
158
		const result = this.indexes;
J
Joao Moreno 已提交
159 160
		const sortedResult = this.sortedIndexes;

J
Joao Moreno 已提交
161
		this.indexes = indexes;
J
Joao Moreno 已提交
162
		this.sortedIndexes = sortedIndexes;
J
Joao Moreno 已提交
163

J
Joao Moreno 已提交
164
		const toRender = disjunction(sortedResult, indexes);
J
Joao Moreno 已提交
165 166
		this.renderer.renderIndexes(toRender);

167
		this._onChange.fire({ indexes, browserEvent });
J
Joao Moreno 已提交
168
		return result;
J
Joao Moreno 已提交
169 170
	}

J
Joao Moreno 已提交
171 172 173 174
	get(): number[] {
		return this.indexes;
	}

J
Joao Moreno 已提交
175
	contains(index: number): boolean {
J
Joao Moreno 已提交
176
		return binarySearch(this.sortedIndexes, index, numericSort) >= 0;
J
Joao Moreno 已提交
177 178
	}

J
Joao Moreno 已提交
179
	dispose() {
180
		dispose(this._onChange);
J
Joao Moreno 已提交
181
	}
J
Joao Moreno 已提交
182 183
}

184
class SelectionTrait<T> extends Trait<T> {
A
Alex Dima 已提交
185

186
	constructor(private setAriaSelected: boolean) {
187
		super('selected');
A
Alex Dima 已提交
188 189
	}

J
Joao Moreno 已提交
190 191
	renderIndex(index: number, container: HTMLElement): void {
		super.renderIndex(index, container);
J
Joao Moreno 已提交
192

193 194 195 196 197 198
		if (this.setAriaSelected) {
			if (this.contains(index)) {
				container.setAttribute('aria-selected', 'true');
			} else {
				container.setAttribute('aria-selected', 'false');
			}
J
Joao Moreno 已提交
199
		}
A
Alex Dima 已提交
200 201 202
	}
}

203 204 205 206 207 208 209 210 211 212
/**
 * The TraitSpliceable is used as a util class to be able
 * to preserve traits across splice calls, given an identity
 * provider.
 */
class TraitSpliceable<T> implements ISpliceable<T> {

	constructor(
		private trait: Trait<T>,
		private view: ListView<T>,
J
Joao Moreno 已提交
213
		private identityProvider?: IIdentityProvider<T>
214 215 216
	) { }

	splice(start: number, deleteCount: number, elements: T[]): void {
J
Joao Moreno 已提交
217
		if (!this.identityProvider) {
J
Joao Moreno 已提交
218
			return this.trait.splice(start, deleteCount, elements.map(() => false));
219 220
		}

J
Joao Moreno 已提交
221 222
		const pastElementsWithTrait = this.trait.get().map(i => this.identityProvider!.getId(this.view.element(i)).toString());
		const elementsWithTrait = elements.map(e => pastElementsWithTrait.indexOf(this.identityProvider!.getId(e).toString()) > -1);
223 224 225 226 227

		this.trait.splice(start, deleteCount, elementsWithTrait);
	}
}

J
Joao Moreno 已提交
228 229 230 231
function isInputElement(e: HTMLElement): boolean {
	return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA';
}

232
class KeyboardController<T> implements IDisposable {
233

M
Matt Bierner 已提交
234
	private readonly disposables = new DisposableStore();
235
	private openController: IOpenController;
J
Joao Moreno 已提交
236 237 238

	constructor(
		private list: List<T>,
J
Joao Moreno 已提交
239 240
		private view: ListView<T>,
		options: IListOptions<T>
J
Joao Moreno 已提交
241
	) {
P
pi1024e 已提交
242
		const multipleSelectionSupport = options.multipleSelectionSupport !== false;
243

244 245
		this.openController = options.openController || DefaultOpenController;

J
Joao Moreno 已提交
246
		const onKeyDown = Event.chain(domEvent(view.domNode, 'keydown'))
J
Joao Moreno 已提交
247
			.filter(e => !isInputElement(e.target as HTMLElement))
J
Joao Moreno 已提交
248 249 250 251 252 253 254
			.map(e => new StandardKeyboardEvent(e));

		onKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(this.onUpArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(this.onDownArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUpArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDownArrow, this, this.disposables);
J
Joao Moreno 已提交
255
		onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables);
J
Joao Moreno 已提交
256 257 258 259

		if (multipleSelectionSupport) {
			onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KEY_A).on(this.onCtrlA, this, this.disposables);
		}
J
Joao Moreno 已提交
260 261
	}

262 263 264
	private onEnter(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
265
		this.list.setSelection(this.list.getFocus(), e.browserEvent);
266 267 268 269

		if (this.openController.shouldOpen(e.browserEvent)) {
			this.list.open(this.list.getFocus(), e.browserEvent);
		}
270 271 272 273 274
	}

	private onUpArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
275
		this.list.focusPrevious(1, false, e.browserEvent);
276 277 278 279 280 281 282
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

	private onDownArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
283
		this.list.focusNext(1, false, e.browserEvent);
284 285 286 287 288 289 290
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

	private onPageUpArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
291
		this.list.focusPreviousPage(e.browserEvent);
292 293 294 295 296 297 298
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

	private onPageDownArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
299
		this.list.focusNextPage(e.browserEvent);
300 301 302 303
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

J
Joao Moreno 已提交
304 305 306
	private onCtrlA(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
307
		this.list.setSelection(range(this.list.length), e.browserEvent);
J
Joao Moreno 已提交
308 309 310 311 312 313
		this.view.domNode.focus();
	}

	private onEscape(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
314
		this.list.setSelection([], e.browserEvent);
J
Joao Moreno 已提交
315 316 317
		this.view.domNode.focus();
	}

J
Joao Moreno 已提交
318
	dispose() {
M
Matt Bierner 已提交
319
		this.disposables.dispose();
J
Joao Moreno 已提交
320 321 322
	}
}

J
Joao Moreno 已提交
323 324 325 326 327
enum TypeLabelControllerState {
	Idle,
	Typing
}

328 329 330 331 332
export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardNavigationDelegate {
	mightProducePrintableCharacter(event: IKeyboardEvent): boolean {
		if (event.ctrlKey || event.metaKey || event.altKey) {
			return false;
		}
J
Joao Moreno 已提交
333

334 335 336 337 338 339
		return (event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z)
			|| (event.keyCode >= KeyCode.KEY_0 && event.keyCode <= KeyCode.KEY_9)
			|| (event.keyCode >= KeyCode.NUMPAD_0 && event.keyCode <= KeyCode.NUMPAD_9)
			|| (event.keyCode >= KeyCode.US_SEMICOLON && event.keyCode <= KeyCode.US_QUOTE);
	}
};
340

341
class TypeLabelController<T> implements IDisposable {
342

343
	private enabled = false;
J
Joao Moreno 已提交
344
	private state: TypeLabelControllerState = TypeLabelControllerState.Idle;
J
Joao Moreno 已提交
345 346 347 348

	private automaticKeyboardNavigation = true;
	private triggered = false;

M
Matt Bierner 已提交
349 350
	private readonly enabledDisposables = new DisposableStore();
	private readonly disposables = new DisposableStore();
J
Joao Moreno 已提交
351 352 353 354

	constructor(
		private list: List<T>,
		private view: ListView<T>,
355 356
		private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>,
		private delegate: IKeyboardNavigationDelegate
J
Joao Moreno 已提交
357
	) {
J
Joao Moreno 已提交
358
		this.updateOptions(list.options);
359 360
	}

J
Joao Moreno 已提交
361
	updateOptions(options: IListOptions<T>): void {
J
Joao Moreno 已提交
362 363 364
		const enableKeyboardNavigation = typeof options.enableKeyboardNavigation === 'undefined' ? true : !!options.enableKeyboardNavigation;

		if (enableKeyboardNavigation) {
365 366 367 368
			this.enable();
		} else {
			this.disable();
		}
J
Joao Moreno 已提交
369 370 371 372 373 374 375 376

		if (typeof options.automaticKeyboardNavigation !== 'undefined') {
			this.automaticKeyboardNavigation = options.automaticKeyboardNavigation;
		}
	}

	toggle(): void {
		this.triggered = !this.triggered;
377 378 379 380 381 382 383 384
	}

	private enable(): void {
		if (this.enabled) {
			return;
		}

		const onChar = Event.chain(domEvent(this.view.domNode, 'keydown'))
J
Joao Moreno 已提交
385
			.filter(e => !isInputElement(e.target as HTMLElement))
J
Joao Moreno 已提交
386
			.filter(() => this.automaticKeyboardNavigation || this.triggered)
J
Joao Moreno 已提交
387
			.map(event => new StandardKeyboardEvent(event))
388
			.filter(e => this.delegate.mightProducePrintableCharacter(e))
J
Joao Moreno 已提交
389
			.forEach(e => { e.stopPropagation(); e.preventDefault(); })
J
Joao Moreno 已提交
390 391 392
			.map(event => event.browserEvent.key)
			.event;

J
Joao Moreno 已提交
393 394
		const onClear = Event.debounce<string, null>(onChar, () => null, 800);
		const onInput = Event.reduce<string | null, string | null>(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i));
J
Joao Moreno 已提交
395

396
		onInput(this.onInput, this, this.enabledDisposables);
397
		onClear(this.onClear, this, this.enabledDisposables);
398 399

		this.enabled = true;
J
Joao Moreno 已提交
400
		this.triggered = false;
401 402 403
	}

	private disable(): void {
P
pi1024e 已提交
404 405
		if (!this.enabled) {
			return;
P
pi1024e 已提交
406
		}
P
pi1024e 已提交
407 408 409 410

		this.enabledDisposables.clear();
		this.enabled = false;
		this.triggered = false;
J
Joao Moreno 已提交
411 412
	}

413 414 415 416 417 418 419 420 421 422
	private onClear(): void {
		const focus = this.list.getFocus();
		if (focus.length > 0) {
			const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0]));
			if (ariaLabel) {
				alert(ariaLabel);
			}
		}
	}

J
Joao Moreno 已提交
423 424 425
	private onInput(word: string | null): void {
		if (!word) {
			this.state = TypeLabelControllerState.Idle;
J
Joao Moreno 已提交
426
			this.triggered = false;
J
Joao Moreno 已提交
427 428 429 430 431 432 433 434 435 436
			return;
		}

		const focus = this.list.getFocus();
		const start = focus.length > 0 ? focus[0] : 0;
		const delta = this.state === TypeLabelControllerState.Idle ? 1 : 0;
		this.state = TypeLabelControllerState.Typing;

		for (let i = 0; i < this.list.length; i++) {
			const index = (start + i + delta) % this.list.length;
J
Joao Moreno 已提交
437
			const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(this.view.element(index));
438
			const labelStr = label && label.toString();
J
Joao Moreno 已提交
439

440
			if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) {
J
Joao Moreno 已提交
441 442 443 444 445 446 447 448
				this.list.setFocus([index]);
				this.list.reveal(index);
				return;
			}
		}
	}

	dispose() {
449
		this.disable();
M
Matt Bierner 已提交
450 451
		this.enabledDisposables.dispose();
		this.disposables.dispose();
J
Joao Moreno 已提交
452 453 454
	}
}

J
Joao Moreno 已提交
455 456
class DOMFocusController<T> implements IDisposable {

M
Matt Bierner 已提交
457
	private readonly disposables = new DisposableStore();
J
Joao Moreno 已提交
458 459 460 461 462

	constructor(
		private list: List<T>,
		private view: ListView<T>
	) {
J
Joao Moreno 已提交
463
		const onKeyDown = Event.chain(domEvent(view.domNode, 'keydown'))
J
Joao Moreno 已提交
464 465 466
			.filter(e => !isInputElement(e.target as HTMLElement))
			.map(e => new StandardKeyboardEvent(e));

J
Joao Moreno 已提交
467 468
		onKeyDown.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey)
			.on(this.onTab, this, this.disposables);
J
Joao Moreno 已提交
469 470 471 472 473 474 475 476 477 478 479 480 481 482
	}

	private onTab(e: StandardKeyboardEvent): void {
		if (e.target !== this.view.domNode) {
			return;
		}

		const focus = this.list.getFocus();

		if (focus.length === 0) {
			return;
		}

		const focusedDomElement = this.view.domElement(focus[0]);
J
Joao Moreno 已提交
483 484 485 486 487

		if (!focusedDomElement) {
			return;
		}

J
Joao Moreno 已提交
488 489
		const tabIndexElement = focusedDomElement.querySelector('[tabIndex]');

490
		if (!tabIndexElement || !(tabIndexElement instanceof HTMLElement) || tabIndexElement.tabIndex === -1) {
I
isidor 已提交
491 492 493 494 495
			return;
		}

		const style = window.getComputedStyle(tabIndexElement);
		if (style.visibility === 'hidden' || style.display === 'none') {
J
Joao Moreno 已提交
496 497 498 499 500 501 502 503 504
			return;
		}

		e.preventDefault();
		e.stopPropagation();
		tabIndexElement.focus();
	}

	dispose() {
M
Matt Bierner 已提交
505
		this.disposables.dispose();
J
Joao Moreno 已提交
506 507 508
	}
}

J
Joao Moreno 已提交
509
export function isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
J
Joao Moreno 已提交
510
	return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;
J
Joao Moreno 已提交
511 512
}

J
Joao Moreno 已提交
513
export function isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
J
Joao Moreno 已提交
514
	return event.browserEvent.shiftKey;
J
Joao Moreno 已提交
515 516
}

517 518
function isMouseRightClick(event: UIEvent): boolean {
	return event instanceof MouseEvent && event.button === 2;
J
Joao Moreno 已提交
519 520
}

T
Tony Xia 已提交
521
const DefaultMultipleSelectionController = {
J
Joao Moreno 已提交
522 523 524 525
	isSelectionSingleChangeEvent,
	isSelectionRangeChangeEvent
};

J
Joao Moreno 已提交
526
const DefaultOpenController: IOpenController = {
B
Benjamin Pasero 已提交
527 528
	shouldOpen: (event: UIEvent) => {
		if (event instanceof MouseEvent) {
529
			return !isMouseRightClick(event);
B
Benjamin Pasero 已提交
530 531 532 533
		}

		return true;
	}
J
Joao Moreno 已提交
534
};
535

536
export class MouseController<T> implements IDisposable {
J
Joao Moreno 已提交
537

J
Joao Moreno 已提交
538
	private multipleSelectionSupport: boolean;
J
Joao Moreno 已提交
539
	readonly multipleSelectionController: IMultipleSelectionController<T> | undefined;
540
	private openController: IOpenController;
J
Joao Moreno 已提交
541
	private mouseSupport: boolean;
M
Matt Bierner 已提交
542
	private readonly disposables = new DisposableStore();
543

544 545
	constructor(protected list: List<T>) {
		this.multipleSelectionSupport = !(list.options.multipleSelectionSupport === false);
J
Joao Moreno 已提交
546 547

		if (this.multipleSelectionSupport) {
T
Tony Xia 已提交
548
			this.multipleSelectionController = list.options.multipleSelectionController || DefaultMultipleSelectionController;
J
Joao Moreno 已提交
549
		}
J
Joao Moreno 已提交
550

551
		this.openController = list.options.openController || DefaultOpenController;
J
Joao Moreno 已提交
552 553 554 555 556 557 558
		this.mouseSupport = typeof list.options.mouseSupport === 'undefined' || !!list.options.mouseSupport;

		if (this.mouseSupport) {
			list.onMouseDown(this.onMouseDown, this, this.disposables);
			list.onContextMenu(this.onContextMenu, this, this.disposables);
			list.onMouseDblClick(this.onDoubleClick, this, this.disposables);
			list.onTouchStart(this.onMouseDown, this, this.disposables);
559
			this.disposables.add(Gesture.addTarget(list.getHTMLElement()));
J
Joao Moreno 已提交
560
		}
J
Joao Moreno 已提交
561

562
		list.onMouseClick(this.onPointer, this, this.disposables);
563
		list.onMouseMiddleClick(this.onPointer, this, this.disposables);
564
		list.onTap(this.onPointer, this, this.disposables);
565 566
	}

J
Joao Moreno 已提交
567
	protected isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
J
Joao Moreno 已提交
568 569
		if (this.multipleSelectionController) {
			return this.multipleSelectionController.isSelectionSingleChangeEvent(event);
570 571 572 573 574
		}

		return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;
	}

J
Joao Moreno 已提交
575
	protected isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
J
Joao Moreno 已提交
576 577 578 579
		if (this.multipleSelectionController) {
			return this.multipleSelectionController.isSelectionRangeChangeEvent(event);
		}

580 581 582 583 584 585 586
		return event.browserEvent.shiftKey;
	}

	private isSelectionChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
		return this.isSelectionSingleChangeEvent(event) || this.isSelectionRangeChangeEvent(event);
	}

J
Joao Moreno 已提交
587
	private onMouseDown(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
J
Joao Moreno 已提交
588
		if (document.activeElement !== e.browserEvent.target) {
589
			this.list.domFocus();
J
Joao Moreno 已提交
590
		}
J
Joao Moreno 已提交
591 592
	}

593
	private onContextMenu(e: IListContextMenuEvent<T>): void {
J
Joao Moreno 已提交
594 595 596
		const focus = typeof e.index === 'undefined' ? [] : [e.index];
		this.list.setFocus(focus, e.browserEvent);
	}
J
Joao Moreno 已提交
597

598
	protected onPointer(e: IListMouseEvent<T>): void {
J
Joao Moreno 已提交
599 600 601 602
		if (!this.mouseSupport) {
			return;
		}

603 604 605 606
		if (isInputElement(e.browserEvent.target as HTMLElement)) {
			return;
		}

J
Joao Moreno 已提交
607
		let reference = this.list.getFocus()[0];
608 609
		const selection = this.list.getSelection();
		reference = reference === undefined ? selection[0] : reference;
J
Joao Moreno 已提交
610

J
Joao Moreno 已提交
611 612 613 614 615 616 617 618
		const focus = e.index;

		if (typeof focus === 'undefined') {
			this.list.setFocus([], e.browserEvent);
			this.list.setSelection([], e.browserEvent);
			return;
		}

619
		if (this.multipleSelectionSupport && this.isSelectionRangeChangeEvent(e)) {
J
Joao Moreno 已提交
620 621 622
			return this.changeSelection(e, reference);
		}

623
		if (this.multipleSelectionSupport && this.isSelectionChangeEvent(e)) {
J
Joao Moreno 已提交
624 625
			return this.changeSelection(e, reference);
		}
626

J
Joao Moreno 已提交
627 628
		this.list.setFocus([focus], e.browserEvent);

J
Joao Moreno 已提交
629
		if (!isMouseRightClick(e.browserEvent)) {
630
			this.list.setSelection([focus], e.browserEvent);
631 632 633 634

			if (this.openController.shouldOpen(e.browserEvent)) {
				this.list.open([focus], e.browserEvent);
			}
635
		}
J
Joao Moreno 已提交
636
	}
J
Joao Moreno 已提交
637

638
	protected onDoubleClick(e: IListMouseEvent<T>): void {
639 640 641 642
		if (isInputElement(e.browserEvent.target as HTMLElement)) {
			return;
		}

643
		if (this.multipleSelectionSupport && this.isSelectionChangeEvent(e)) {
J
Joao Moreno 已提交
644 645
			return;
		}
646

J
Joao Moreno 已提交
647
		const focus = this.list.getFocus();
648
		this.list.setSelection(focus, e.browserEvent);
J
Joao Moreno 已提交
649
		this.list.pin(focus);
J
Joao Moreno 已提交
650
	}
651

J
Joao Moreno 已提交
652
	private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>, reference: number | undefined): void {
J
Joao Moreno 已提交
653
		const focus = e.index!;
654

655
		if (this.isSelectionRangeChangeEvent(e) && reference !== undefined) {
J
Joao Moreno 已提交
656 657
			const min = Math.min(reference, focus);
			const max = Math.max(reference, focus);
J
Joao Moreno 已提交
658
			const rangeSelection = range(min, max + 1);
J
Joao Moreno 已提交
659 660
			const selection = this.list.getSelection();
			const contiguousRange = getContiguousRangeContaining(disjunction(selection, [reference]), reference);
661

J
Joao Moreno 已提交
662 663
			if (contiguousRange.length === 0) {
				return;
664 665
			}

J
Joao Moreno 已提交
666
			const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange));
667
			this.list.setSelection(newSelection, e.browserEvent);
J
Joao Moreno 已提交
668

669
		} else if (this.isSelectionSingleChangeEvent(e)) {
J
Joao Moreno 已提交
670 671 672
			const selection = this.list.getSelection();
			const newSelection = selection.filter(i => i !== focus);

J
Joao Moreno 已提交
673 674
			this.list.setFocus([focus]);

J
Joao Moreno 已提交
675
			if (selection.length === newSelection.length) {
676
				this.list.setSelection([...newSelection, focus], e.browserEvent);
J
Joao Moreno 已提交
677
			} else {
678
				this.list.setSelection(newSelection, e.browserEvent);
J
Joao Moreno 已提交
679 680
			}
		}
681 682 683
	}

	dispose() {
M
Matt Bierner 已提交
684
		this.disposables.dispose();
685 686 687
	}
}

J
Joao Moreno 已提交
688 689 690
export interface IMultipleSelectionController<T> {
	isSelectionSingleChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;
	isSelectionRangeChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;
691 692
}

693 694 695 696
export interface IOpenController {
	shouldOpen(event: UIEvent): boolean;
}

697 698 699 700
export interface IStyleController {
	style(styles: IListStyles): void;
}

J
João Moreno 已提交
701
export interface IListAccessibilityProvider<T> extends IListViewAccessibilityProvider<T> {
702
	getAriaLabel(element: T): string | null;
703 704
	getWidgetAriaLabel(): string;
	getWidgetRole?(): string;
J
Joao Moreno 已提交
705
	getAriaLevel?(element: T): number | undefined;
J
Joao Moreno 已提交
706 707
	onDidChangeActiveDescendant?: Event<void>;
	getActiveDescendantId?(element: T): string | undefined;
708 709
}

710 711
export class DefaultStyleController implements IStyleController {

712
	constructor(private styleElement: HTMLStyleElement, private selectorSuffix: string) { }
713 714

	style(styles: IListStyles): void {
715
		const suffix = this.selectorSuffix && `.${this.selectorSuffix}`;
716 717
		const content: string[] = [];

718 719 720
		if (styles.listBackground) {
			if (styles.listBackground.isOpaque()) {
				content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`);
J
Joao Moreno 已提交
721
			} else if (!platform.isMacintosh) { // subpixel AA doesn't exist in macOS
722 723 724 725
				console.warn(`List with id '${this.selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`);
			}
		}

726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
		if (styles.listFocusBackground) {
			content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`);
			content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case!
		}

		if (styles.listFocusForeground) {
			content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`);
		}

		if (styles.listActiveSelectionBackground) {
			content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`);
			content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case!
		}

		if (styles.listActiveSelectionForeground) {
			content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`);
		}

		if (styles.listFocusAndSelectionBackground) {
J
Joao Moreno 已提交
745
			content.push(`
J
Joao Moreno 已提交
746
				.monaco-drag-image,
J
Joao Moreno 已提交
747 748
				.monaco-list${suffix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }
			`);
749 750 751
		}

		if (styles.listFocusAndSelectionForeground) {
J
Joao Moreno 已提交
752
			content.push(`
J
Joao Moreno 已提交
753
				.monaco-drag-image,
J
Joao Moreno 已提交
754 755
				.monaco-list${suffix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }
			`);
756 757
		}

758 759 760 761 762
		if (styles.listInactiveFocusBackground) {
			content.push(`.monaco-list${suffix} .monaco-list-row.focused { background-color:  ${styles.listInactiveFocusBackground}; }`);
			content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { background-color:  ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!
		}

763 764 765 766 767 768 769 770 771 772
		if (styles.listInactiveSelectionBackground) {
			content.push(`.monaco-list${suffix} .monaco-list-row.selected { background-color:  ${styles.listInactiveSelectionBackground}; }`);
			content.push(`.monaco-list${suffix} .monaco-list-row.selected:hover { background-color:  ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case!
		}

		if (styles.listInactiveSelectionForeground) {
			content.push(`.monaco-list${suffix} .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`);
		}

		if (styles.listHoverBackground) {
J
Joao Moreno 已提交
773
			content.push(`.monaco-list${suffix}:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) { background-color:  ${styles.listHoverBackground}; }`);
774 775 776
		}

		if (styles.listHoverForeground) {
J
Joao Moreno 已提交
777
			content.push(`.monaco-list${suffix} .monaco-list-row:hover:not(.selected):not(.focused) { color:  ${styles.listHoverForeground}; }`);
778 779 780 781 782 783 784
		}

		if (styles.listSelectionOutline) {
			content.push(`.monaco-list${suffix} .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`);
		}

		if (styles.listFocusOutline) {
J
Joao Moreno 已提交
785
			content.push(`
J
Joao Moreno 已提交
786
				.monaco-drag-image,
J
Joao Moreno 已提交
787 788
				.monaco-list${suffix}:focus .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }
			`);
789 790 791 792 793 794 795 796 797 798
		}

		if (styles.listInactiveFocusOutline) {
			content.push(`.monaco-list${suffix} .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`);
		}

		if (styles.listHoverOutline) {
			content.push(`.monaco-list${suffix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);
		}

J
Joao Moreno 已提交
799 800 801
		if (styles.listDropBackground) {
			content.push(`
				.monaco-list${suffix}.drop-target,
J
Joao Moreno 已提交
802
				.monaco-list${suffix} .monaco-list-rows.drop-target,
J
Joao Moreno 已提交
803 804 805
				.monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; }
			`);
		}
J
Joao Moreno 已提交
806

J
Joao Moreno 已提交
807 808
		if (styles.listFilterWidgetBackground) {
			content.push(`.monaco-list-type-filter { background-color: ${styles.listFilterWidgetBackground} }`);
J
Joao Moreno 已提交
809 810
		}

J
Joao Moreno 已提交
811 812
		if (styles.listFilterWidgetOutline) {
			content.push(`.monaco-list-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`);
J
Joao Moreno 已提交
813 814
		}

J
Joao Moreno 已提交
815 816
		if (styles.listFilterWidgetNoMatchesOutline) {
			content.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`);
817 818
		}

J
Joao Moreno 已提交
819
		if (styles.listMatchesShadow) {
J
Joao Moreno 已提交
820
			content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`);
J
Joao Moreno 已提交
821 822
		}

823 824 825 826 827 828 829
		const newStyles = content.join('\n');
		if (newStyles !== this.styleElement.innerHTML) {
			this.styleElement.innerHTML = newStyles;
		}
	}
}

830
export interface IListOptions<T> {
J
Joao Moreno 已提交
831
	readonly identityProvider?: IIdentityProvider<T>;
832
	readonly dnd?: IListDragAndDrop<T>;
833
	readonly enableKeyboardNavigation?: boolean;
J
Joao Moreno 已提交
834
	readonly automaticKeyboardNavigation?: boolean;
J
Joao Moreno 已提交
835
	readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider<T>;
836
	readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate;
J
Joao Moreno 已提交
837 838 839 840
	readonly keyboardSupport?: boolean;
	readonly multipleSelectionSupport?: boolean;
	readonly multipleSelectionController?: IMultipleSelectionController<T>;
	readonly openController?: IOpenController;
841
	readonly styleController?: (suffix: string) => IStyleController;
J
João Moreno 已提交
842
	readonly accessibilityProvider?: IListAccessibilityProvider<T>;
J
Joao Moreno 已提交
843 844 845 846 847

	// list view options
	readonly useShadows?: boolean;
	readonly verticalScrollMode?: ScrollbarVisibility;
	readonly setRowLineHeight?: boolean;
R
rebornix 已提交
848
	readonly setRowHeight?: boolean;
J
Joao Moreno 已提交
849 850
	readonly supportDynamicHeights?: boolean;
	readonly mouseSupport?: boolean;
851
	readonly horizontalScrolling?: boolean;
852
	readonly additionalScrollHeight?: number;
853
	readonly transformOptimization?: boolean;
J
Joao Moreno 已提交
854 855
}

856
export interface IListStyles {
857
	listBackground?: Color;
858
	listFocusBackground?: Color;
859
	listFocusForeground?: Color;
860 861 862 863 864
	listActiveSelectionBackground?: Color;
	listActiveSelectionForeground?: Color;
	listFocusAndSelectionBackground?: Color;
	listFocusAndSelectionForeground?: Color;
	listInactiveSelectionBackground?: Color;
865
	listInactiveSelectionForeground?: Color;
M
Martin Aeschlimann 已提交
866
	listInactiveFocusBackground?: Color;
867
	listHoverBackground?: Color;
868
	listHoverForeground?: Color;
869 870
	listDropBackground?: Color;
	listFocusOutline?: Color;
871 872 873
	listInactiveFocusOutline?: Color;
	listSelectionOutline?: Color;
	listHoverOutline?: Color;
J
Joao Moreno 已提交
874 875 876
	listFilterWidgetBackground?: Color;
	listFilterWidgetOutline?: Color;
	listFilterWidgetNoMatchesOutline?: Color;
J
Joao Moreno 已提交
877
	listMatchesShadow?: Color;
J
Joao Moreno 已提交
878
	treeIndentGuidesStroke?: Color;
879 880 881
}

const defaultStyles: IListStyles = {
J
Joao Moreno 已提交
882
	listFocusBackground: Color.fromHex('#7FB0D0'),
883 884 885 886 887 888
	listActiveSelectionBackground: Color.fromHex('#0E639C'),
	listActiveSelectionForeground: Color.fromHex('#FFFFFF'),
	listFocusAndSelectionBackground: Color.fromHex('#094771'),
	listFocusAndSelectionForeground: Color.fromHex('#FFFFFF'),
	listInactiveSelectionBackground: Color.fromHex('#3F3F46'),
	listHoverBackground: Color.fromHex('#2A2D2E'),
J
Joao Moreno 已提交
889 890
	listDropBackground: Color.fromHex('#383B3D'),
	treeIndentGuidesStroke: Color.fromHex('#a9a9a9')
891 892
};

J
João Moreno 已提交
893
const DefaultOptions: IListOptions<any> = {
894
	keyboardSupport: true,
I
isidor 已提交
895
	mouseSupport: true,
J
Joao Moreno 已提交
896 897 898 899 900 901
	multipleSelectionSupport: true,
	dnd: {
		getDragURI() { return null; },
		onDragStart(): void { },
		onDragOver() { return false; },
		drop() { }
J
João Moreno 已提交
902
	}
903
};
J
Joao Moreno 已提交
904

J
Joao Moreno 已提交
905 906
// TODO@Joao: move these utils into a SortedArray class

907 908 909 910 911 912 913
function getContiguousRangeContaining(range: number[], value: number): number[] {
	const index = range.indexOf(value);

	if (index === -1) {
		return [];
	}

M
Matt Bierner 已提交
914
	const result: number[] = [];
915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930
	let i = index - 1;
	while (i >= 0 && range[i] === value - (index - i)) {
		result.push(range[i--]);
	}

	result.reverse();
	i = index;
	while (i < range.length && range[i] === value + (i - index)) {
		result.push(range[i++]);
	}

	return result;
}

/**
 * Given two sorted collections of numbers, returns the intersection
T
Tony Xia 已提交
931
 * between them (OR).
932 933
 */
function disjunction(one: number[], other: number[]): number[] {
M
Matt Bierner 已提交
934
	const result: number[] = [];
935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961
	let i = 0, j = 0;

	while (i < one.length || j < other.length) {
		if (i >= one.length) {
			result.push(other[j++]);
		} else if (j >= other.length) {
			result.push(one[i++]);
		} else if (one[i] === other[j]) {
			result.push(one[i]);
			i++;
			j++;
			continue;
		} else if (one[i] < other[j]) {
			result.push(one[i++]);
		} else {
			result.push(other[j++]);
		}
	}

	return result;
}

/**
 * Given two sorted collections of numbers, returns the relative
 * complement between them (XOR).
 */
function relativeComplement(one: number[], other: number[]): number[] {
M
Matt Bierner 已提交
962
	const result: number[] = [];
963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983
	let i = 0, j = 0;

	while (i < one.length || j < other.length) {
		if (i >= one.length) {
			result.push(other[j++]);
		} else if (j >= other.length) {
			result.push(one[i++]);
		} else if (one[i] === other[j]) {
			i++;
			j++;
			continue;
		} else if (one[i] < other[j]) {
			result.push(one[i++]);
		} else {
			j++;
		}
	}

	return result;
}

984 985
const numericSort = (a: number, b: number) => a - b;

J
Joao Moreno 已提交
986
class PipelineRenderer<T> implements IListRenderer<T, any> {
J
Joao Moreno 已提交
987 988 989

	constructor(
		private _templateId: string,
J
Joao Moreno 已提交
990
		private renderers: IListRenderer<any /* TODO@joao */, any>[]
J
Joao Moreno 已提交
991 992 993 994 995 996 997 998 999 1000
	) { }

	get templateId(): string {
		return this._templateId;
	}

	renderTemplate(container: HTMLElement): any[] {
		return this.renderers.map(r => r.renderTemplate(container));
	}

1001
	renderElement(element: T, index: number, templateData: any[], height: number | undefined): void {
J
Joao Moreno 已提交
1002 1003 1004
		let i = 0;

		for (const renderer of this.renderers) {
1005
			renderer.renderElement(element, index, templateData[i++], height);
J
Joao Moreno 已提交
1006
		}
J
Joao Moreno 已提交
1007 1008
	}

1009
	disposeElement(element: T, index: number, templateData: any[], height: number | undefined): void {
J
Joao Moreno 已提交
1010 1011 1012
		let i = 0;

		for (const renderer of this.renderers) {
J
Joao Moreno 已提交
1013
			if (renderer.disposeElement) {
1014
				renderer.disposeElement(element, index, templateData[i], height);
J
Joao Moreno 已提交
1015
			}
J
fix npe  
Joao Moreno 已提交
1016 1017

			i += 1;
J
Joao Moreno 已提交
1018 1019 1020
		}
	}

J
Joao Moreno 已提交
1021
	disposeTemplate(templateData: any[]): void {
J
Joao Moreno 已提交
1022 1023 1024
		let i = 0;

		for (const renderer of this.renderers) {
J
Joao Moreno 已提交
1025
			renderer.disposeTemplate(templateData[i++]);
J
Joao Moreno 已提交
1026
		}
J
Joao Moreno 已提交
1027 1028 1029
	}
}

1030 1031 1032 1033
class AccessibiltyRenderer<T> implements IListRenderer<T, HTMLElement> {

	templateId: string = 'a18n';

J
João Moreno 已提交
1034
	constructor(private accessibilityProvider: IListAccessibilityProvider<T>) { }
1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047

	renderTemplate(container: HTMLElement): HTMLElement {
		return container;
	}

	renderElement(element: T, index: number, container: HTMLElement): void {
		const ariaLabel = this.accessibilityProvider.getAriaLabel(element);

		if (ariaLabel) {
			container.setAttribute('aria-label', ariaLabel);
		} else {
			container.removeAttribute('aria-label');
		}
J
Joao Moreno 已提交
1048 1049 1050 1051 1052 1053 1054 1055

		const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element);

		if (typeof ariaLevel === 'number') {
			container.setAttribute('aria-level', `${ariaLevel}`);
		} else {
			container.removeAttribute('aria-level');
		}
1056 1057 1058 1059 1060 1061 1062
	}

	disposeTemplate(templateData: any): void {
		// noop
	}
}

J
Joao Moreno 已提交
1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076
class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {

	constructor(private list: List<T>, private dnd: IListDragAndDrop<T>) { }

	getDragElements(element: T): T[] {
		const selection = this.list.getSelectedElements();
		const elements = selection.indexOf(element) > -1 ? selection : [element];
		return elements;
	}

	getDragURI(element: T): string | null {
		return this.dnd.getDragURI(element);
	}

J
Joao Moreno 已提交
1077
	getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined {
J
Joao Moreno 已提交
1078
		if (this.dnd.getDragLabel) {
J
Joao Moreno 已提交
1079
			return this.dnd.getDragLabel(elements, originalEvent);
J
Joao Moreno 已提交
1080 1081 1082
		}

		return undefined;
J
Joao Moreno 已提交
1083 1084 1085
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
J
Joao Moreno 已提交
1086 1087 1088
		if (this.dnd.onDragStart) {
			this.dnd.onDragStart(data, originalEvent);
		}
J
Joao Moreno 已提交
1089 1090 1091 1092 1093 1094
	}

	onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): boolean | IListDragOverReaction {
		return this.dnd.onDragOver(data, targetElement, targetIndex, originalEvent);
	}

J
Joao Moreno 已提交
1095 1096 1097 1098 1099 1100
	onDragEnd(originalEvent: DragEvent): void {
		if (this.dnd.onDragEnd) {
			this.dnd.onDragEnd(originalEvent);
		}
	}

J
Joao Moreno 已提交
1101 1102 1103 1104 1105
	drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
		this.dnd.drop(data, targetElement, targetIndex, originalEvent);
	}
}

1106 1107
export interface IListOptionsUpdate {
	readonly enableKeyboardNavigation?: boolean;
J
Joao Moreno 已提交
1108
	readonly automaticKeyboardNavigation?: boolean;
1109
	readonly additionalScrollHeight?: number;
1110 1111
}

1112
export class List<T> implements ISpliceable<T>, IDisposable {
J
Joao Moreno 已提交
1113

A
Alex Dima 已提交
1114 1115
	private focus: Trait<T>;
	private selection: Trait<T>;
J
Joao Moreno 已提交
1116
	private eventBufferer = new EventBufferer();
R
rebornix 已提交
1117
	protected view: ListView<T>;
1118
	private spliceable: ISpliceable<T>;
1119
	private styleController: IStyleController;
J
Joao Moreno 已提交
1120
	private typeLabelController?: TypeLabelController<T>;
J
João Moreno 已提交
1121
	private accessibilityProvider?: IListAccessibilityProvider<T>;
1122
	private _ariaLabel: string = '';
1123

M
Matt Bierner 已提交
1124
	protected readonly disposables = new DisposableStore();
J
Joao Moreno 已提交
1125

1126
	@memoize get onDidChangeFocus(): Event<IListEvent<T>> {
J
Joao Moreno 已提交
1127
		return Event.map(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toListEvent(e));
J
Joao Moreno 已提交
1128 1129
	}

1130
	@memoize get onDidChangeSelection(): Event<IListEvent<T>> {
J
Joao Moreno 已提交
1131
		return Event.map(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toListEvent(e));
J
Joao Moreno 已提交
1132 1133
	}

1134
	private readonly _onDidOpen = new Emitter<IListEvent<T>>();
J
Joao Moreno 已提交
1135
	readonly onDidOpen: Event<IListEvent<T>> = this._onDidOpen.event;
J
Joao Moreno 已提交
1136

1137
	private readonly _onDidPin = new Emitter<IListEvent<T>>();
J
Joao Moreno 已提交
1138
	readonly onDidPin: Event<IListEvent<T>> = this._onDidPin.event;
J
Joao Moreno 已提交
1139

J
Joao Moreno 已提交
1140
	get domId(): string { return this.view.domId; }
J
Joao Moreno 已提交
1141
	get onDidScroll(): Event<ScrollEvent> { return this.view.onDidScroll; }
J
Joao Moreno 已提交
1142 1143
	get onMouseClick(): Event<IListMouseEvent<T>> { return this.view.onMouseClick; }
	get onMouseDblClick(): Event<IListMouseEvent<T>> { return this.view.onMouseDblClick; }
1144
	get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return this.view.onMouseMiddleClick; }
J
Joao Moreno 已提交
1145 1146 1147 1148 1149 1150 1151 1152
	get onMouseUp(): Event<IListMouseEvent<T>> { return this.view.onMouseUp; }
	get onMouseDown(): Event<IListMouseEvent<T>> { return this.view.onMouseDown; }
	get onMouseOver(): Event<IListMouseEvent<T>> { return this.view.onMouseOver; }
	get onMouseMove(): Event<IListMouseEvent<T>> { return this.view.onMouseMove; }
	get onMouseOut(): Event<IListMouseEvent<T>> { return this.view.onMouseOut; }
	get onTouchStart(): Event<IListTouchEvent<T>> { return this.view.onTouchStart; }
	get onTap(): Event<IListGestureEvent<T>> { return this.view.onTap; }

J
Joao Moreno 已提交
1153 1154
	private didJustPressContextMenuKey: boolean = false;
	@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
J
Joao Moreno 已提交
1155
		const fromKeydown = Event.chain(domEvent(this.view.domNode, 'keydown'))
J
Joao Moreno 已提交
1156 1157 1158
			.map(e => new StandardKeyboardEvent(e))
			.filter(e => this.didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
			.filter(e => { e.preventDefault(); e.stopPropagation(); return false; })
1159
			.event as Event<any>;
J
Joao Moreno 已提交
1160

J
Joao Moreno 已提交
1161
		const fromKeyup = Event.chain(domEvent(this.view.domNode, 'keyup'))
J
Joao Moreno 已提交
1162 1163 1164 1165 1166
			.filter(() => {
				const didJustPressContextMenuKey = this.didJustPressContextMenuKey;
				this.didJustPressContextMenuKey = false;
				return didJustPressContextMenuKey;
			})
1167
			.filter(() => this.getFocus().length > 0 && !!this.view.domElement(this.getFocus()[0]))
J
Joao Moreno 已提交
1168 1169 1170
			.map(browserEvent => {
				const index = this.getFocus()[0];
				const element = this.view.element(index);
1171
				const anchor = this.view.domElement(index) as HTMLElement;
J
Joao Moreno 已提交
1172 1173 1174 1175
				return { index, element, anchor, browserEvent };
			})
			.event;

J
Joao Moreno 已提交
1176
		const fromMouse = Event.chain(this.view.onContextMenu)
J
Joao Moreno 已提交
1177 1178 1179 1180
			.filter(() => !this.didJustPressContextMenuKey)
			.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent }))
			.event;

J
Joao Moreno 已提交
1181
		return Event.any<IListContextMenuEvent<T>>(fromKeydown, fromKeyup, fromMouse);
J
Joao Moreno 已提交
1182 1183
	}

J
Joao Moreno 已提交
1184 1185 1186
	get onKeyDown(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keydown'); }
	get onKeyUp(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keyup'); }
	get onKeyPress(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keypress'); }
J
Joao Moreno 已提交
1187

1188 1189
	readonly onDidFocus: Event<void>;
	readonly onDidBlur: Event<void>;
1190

1191
	private readonly _onDidDispose = new Emitter<void>();
1192
	readonly onDidDispose: Event<void> = this._onDidDispose.event;
1193

J
Joao Moreno 已提交
1194
	constructor(
J
Joao Moreno 已提交
1195
		private user: string,
J
Joao Moreno 已提交
1196
		container: HTMLElement,
J
Joao Moreno 已提交
1197
		virtualDelegate: IListVirtualDelegate<T>,
J
Joao Moreno 已提交
1198
		renderers: IListRenderer<any /* TODO@joao */, any>[],
1199
		private _options: IListOptions<T> = DefaultOptions
J
Joao Moreno 已提交
1200
	) {
1201 1202
		const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list';
		this.selection = new SelectionTrait(role !== 'listbox');
1203
		this.focus = new Trait('focused');
1204

1205
		mixin(_options, defaultStyles, false);
J
Joao Moreno 已提交
1206

1207 1208
		const baseRenderers: IListRenderer<T, ITraitTemplateData>[] = [this.focus.renderer, this.selection.renderer];

J
Joao Moreno 已提交
1209 1210 1211 1212 1213 1214 1215 1216
		this.accessibilityProvider = _options.accessibilityProvider;

		if (this.accessibilityProvider) {
			baseRenderers.push(new AccessibiltyRenderer<T>(this.accessibilityProvider));

			if (this.accessibilityProvider.onDidChangeActiveDescendant) {
				this.accessibilityProvider.onDidChangeActiveDescendant(this.onDidChangeActiveDescendant, this, this.disposables);
			}
1217 1218 1219
		}

		renderers = renderers.map(r => new PipelineRenderer(r.templateId, [...baseRenderers, r]));
J
Joao Moreno 已提交
1220

J
Joao Moreno 已提交
1221
		const viewOptions: IListViewOptions<T> = {
1222 1223
			..._options,
			dnd: _options.dnd && new ListViewDragAndDrop(this, _options.dnd)
J
Joao Moreno 已提交
1224 1225 1226
		};

		this.view = new ListView(container, virtualDelegate, renderers, viewOptions);
1227
		this.view.domNode.setAttribute('role', role);
1228

1229 1230 1231 1232 1233 1234
		if (_options.styleController) {
			this.styleController = _options.styleController(this.view.domId);
		} else {
			const styleElement = DOM.createStyleSheet(this.view.domNode);
			this.styleController = new DefaultStyleController(styleElement, this.view.domId);
		}
1235

1236
		this.spliceable = new CombinedSpliceable([
1237 1238
			new TraitSpliceable(this.focus, this.view, _options.identityProvider),
			new TraitSpliceable(this.selection, this.view, _options.identityProvider),
1239 1240 1241
			this.view
		]);

M
Matt Bierner 已提交
1242 1243 1244 1245
		this.disposables.add(this.focus);
		this.disposables.add(this.selection);
		this.disposables.add(this.view);
		this.disposables.add(this._onDidDispose);
1246

J
Joao Moreno 已提交
1247 1248
		this.onDidFocus = Event.map(domEvent(this.view.domNode, 'focus', true), () => null!);
		this.onDidBlur = Event.map(domEvent(this.view.domNode, 'blur', true), () => null!);
1249

M
Matt Bierner 已提交
1250
		this.disposables.add(new DOMFocusController(this, this.view));
J
Joao Moreno 已提交
1251

1252 1253
		if (typeof _options.keyboardSupport !== 'boolean' || _options.keyboardSupport) {
			const controller = new KeyboardController(this, this.view, _options);
M
Matt Bierner 已提交
1254
			this.disposables.add(controller);
1255 1256
		}

1257
		if (_options.keyboardNavigationLabelProvider) {
1258 1259
			const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;
			this.typeLabelController = new TypeLabelController(this, this.view, _options.keyboardNavigationLabelProvider, delegate);
M
Matt Bierner 已提交
1260
			this.disposables.add(this.typeLabelController);
J
Joao Moreno 已提交
1261 1262
		}

M
Matt Bierner 已提交
1263
		this.disposables.add(this.createMouseController(_options));
1264

1265 1266
		this.onDidChangeFocus(this._onFocusChange, this, this.disposables);
		this.onDidChangeSelection(this._onSelectionChange, this, this.disposables);
J
João Moreno 已提交
1267

1268 1269
		if (this.accessibilityProvider) {
			this.ariaLabel = this.accessibilityProvider.getWidgetAriaLabel();
J
João Moreno 已提交
1270
		}
1271 1272 1273
		if (_options.multipleSelectionSupport) {
			this.view.domNode.setAttribute('aria-multiselectable', 'true');
		}
1274 1275
	}

1276 1277 1278 1279
	protected createMouseController(options: IListOptions<T>): MouseController<T> {
		return new MouseController(this);
	}

1280 1281
	updateOptions(optionsUpdate: IListOptionsUpdate = {}): void {
		this._options = { ...this._options, ...optionsUpdate };
J
Joao Moreno 已提交
1282 1283 1284 1285

		if (this.typeLabelController) {
			this.typeLabelController.updateOptions(this._options);
		}
1286 1287 1288 1289

		if (optionsUpdate.additionalScrollHeight !== undefined) {
			this.view.updateOptions(optionsUpdate);
		}
1290 1291 1292 1293
	}

	get options(): IListOptions<T> {
		return this._options;
J
Joao Moreno 已提交
1294 1295
	}

J
Joao Moreno 已提交
1296
	splice(start: number, deleteCount: number, elements: T[] = []): void {
J
Joao Moreno 已提交
1297
		if (start < 0 || start > this.view.length) {
J
Joao Moreno 已提交
1298
			throw new ListError(this.user, `Invalid start index: ${start}`);
J
Joao Moreno 已提交
1299 1300 1301
		}

		if (deleteCount < 0) {
J
Joao Moreno 已提交
1302
			throw new ListError(this.user, `Invalid delete count: ${deleteCount}`);
J
Joao Moreno 已提交
1303 1304
		}

J
Joao Moreno 已提交
1305 1306 1307 1308
		if (deleteCount === 0 && elements.length === 0) {
			return;
		}

1309
		this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements));
J
Joao Moreno 已提交
1310 1311
	}

J
Joao Moreno 已提交
1312 1313 1314 1315
	updateWidth(index: number): void {
		this.view.updateWidth(index);
	}

1316
	updateElementHeight(index: number, size: number): void {
1317
		this.view.updateElementHeight(index, size, null);
1318 1319
	}

I
isidor 已提交
1320 1321 1322 1323
	rerender(): void {
		this.view.rerender();
	}

J
Joao Moreno 已提交
1324 1325 1326 1327
	element(index: number): T {
		return this.view.element(index);
	}

J
Joao Moreno 已提交
1328 1329 1330 1331
	get length(): number {
		return this.view.length;
	}

J
Joao Moreno 已提交
1332
	get contentHeight(): number {
J
Joao Moreno 已提交
1333 1334 1335 1336 1337
		return this.view.contentHeight;
	}

	get onDidChangeContentHeight(): Event<number> {
		return this.view.onDidChangeContentHeight;
J
Joao Moreno 已提交
1338 1339
	}

J
Joao Moreno 已提交
1340 1341 1342 1343
	get scrollTop(): number {
		return this.view.getScrollTop();
	}

J
Joao Moreno 已提交
1344 1345 1346 1347
	set scrollTop(scrollTop: number) {
		this.view.setScrollTop(scrollTop);
	}

I
isidor 已提交
1348 1349 1350 1351 1352
	get scrollLeft(): number {
		return this.view.getScrollLeft();
	}

	set scrollLeft(scrollLeft: number) {
J
Joao Moreno 已提交
1353
		this.view.setScrollLeft(scrollLeft);
I
isidor 已提交
1354 1355
	}

J
Joao Moreno 已提交
1356 1357 1358 1359
	get scrollHeight(): number {
		return this.view.scrollHeight;
	}

I
isidor 已提交
1360 1361 1362 1363
	get renderHeight(): number {
		return this.view.renderHeight;
	}

J
Joao Moreno 已提交
1364 1365 1366 1367 1368 1369 1370 1371
	get firstVisibleIndex(): number {
		return this.view.firstVisibleIndex;
	}

	get lastVisibleIndex(): number {
		return this.view.lastVisibleIndex;
	}

1372 1373 1374 1375 1376 1377 1378 1379 1380
	get ariaLabel(): string {
		return this._ariaLabel;
	}

	set ariaLabel(value: string) {
		this._ariaLabel = value;
		this.view.domNode.setAttribute('aria-label', localize('aria list', "{0}. Use the navigation keys to navigate.", value));
	}

J
Joao Moreno 已提交
1381 1382 1383 1384
	domFocus(): void {
		this.view.domNode.focus();
	}

1385 1386
	layout(height?: number, width?: number): void {
		this.view.layout(height, width);
J
Joao Moreno 已提交
1387 1388
	}

J
Joao Moreno 已提交
1389 1390 1391 1392 1393 1394
	toggleKeyboardNavigation(): void {
		if (this.typeLabelController) {
			this.typeLabelController.toggle();
		}
	}

1395
	setSelection(indexes: number[], browserEvent?: UIEvent): void {
J
Joao Moreno 已提交
1396 1397
		for (const index of indexes) {
			if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1398
				throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1399 1400 1401
			}
		}

1402
		this.selection.set(indexes, browserEvent);
J
Joao Moreno 已提交
1403 1404
	}

J
Joao Moreno 已提交
1405 1406 1407 1408
	getSelection(): number[] {
		return this.selection.get();
	}

1409 1410 1411 1412
	getSelectedElements(): T[] {
		return this.getSelection().map(i => this.view.element(i));
	}

1413
	setFocus(indexes: number[], browserEvent?: UIEvent): void {
J
Joao Moreno 已提交
1414 1415
		for (const index of indexes) {
			if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1416
				throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1417 1418 1419
			}
		}

1420
		this.focus.set(indexes, browserEvent);
J
Joao Moreno 已提交
1421 1422
	}

J
Joao Moreno 已提交
1423
	focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
J
Joao Moreno 已提交
1424
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
1425

J
Joao Moreno 已提交
1426
		const focus = this.focus.get();
J
Joao Moreno 已提交
1427 1428 1429 1430 1431
		const index = this.findNextIndex(focus.length > 0 ? focus[0] + n : 0, loop, filter);

		if (index > -1) {
			this.setFocus([index], browserEvent);
		}
J
Joao Moreno 已提交
1432 1433
	}

J
Joao Moreno 已提交
1434
	focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
J
Joao Moreno 已提交
1435
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
1436

J
Joao Moreno 已提交
1437
		const focus = this.focus.get();
J
Joao Moreno 已提交
1438 1439 1440 1441 1442
		const index = this.findPreviousIndex(focus.length > 0 ? focus[0] - n : 0, loop, filter);

		if (index > -1) {
			this.setFocus([index], browserEvent);
		}
J
Joao Moreno 已提交
1443 1444
	}

J
Joao Moreno 已提交
1445
	focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
J
Joao Moreno 已提交
1446 1447 1448
		let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight);
		lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;
		const lastPageElement = this.view.element(lastPageIndex);
J
Joao Moreno 已提交
1449
		const currentlyFocusedElement = this.getFocusedElements()[0];
J
Joao Moreno 已提交
1450 1451

		if (currentlyFocusedElement !== lastPageElement) {
J
Joao Moreno 已提交
1452 1453 1454 1455 1456 1457 1458
			const lastGoodPageIndex = this.findPreviousIndex(lastPageIndex, false, filter);

			if (lastGoodPageIndex > -1 && currentlyFocusedElement !== this.view.element(lastGoodPageIndex)) {
				this.setFocus([lastGoodPageIndex], browserEvent);
			} else {
				this.setFocus([lastPageIndex], browserEvent);
			}
J
Joao Moreno 已提交
1459 1460
		} else {
			const previousScrollTop = this.view.getScrollTop();
J
Joao Moreno 已提交
1461
			this.view.setScrollTop(previousScrollTop + this.view.renderHeight - this.view.elementHeight(lastPageIndex));
J
Joao Moreno 已提交
1462 1463 1464

			if (this.view.getScrollTop() !== previousScrollTop) {
				// Let the scroll event listener run
J
Joao Moreno 已提交
1465
				setTimeout(() => this.focusNextPage(browserEvent, filter), 0);
J
Joao Moreno 已提交
1466 1467 1468 1469
			}
		}
	}

J
Joao Moreno 已提交
1470
	focusPreviousPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
J
Johannes Rieken 已提交
1471
		let firstPageIndex: number;
J
Joao Moreno 已提交
1472 1473 1474 1475 1476 1477 1478 1479 1480
		const scrollTop = this.view.getScrollTop();

		if (scrollTop === 0) {
			firstPageIndex = this.view.indexAt(scrollTop);
		} else {
			firstPageIndex = this.view.indexAfter(scrollTop - 1);
		}

		const firstPageElement = this.view.element(firstPageIndex);
J
Joao Moreno 已提交
1481
		const currentlyFocusedElement = this.getFocusedElements()[0];
J
Joao Moreno 已提交
1482 1483

		if (currentlyFocusedElement !== firstPageElement) {
J
Joao Moreno 已提交
1484 1485 1486 1487 1488 1489 1490
			const firstGoodPageIndex = this.findNextIndex(firstPageIndex, false, filter);

			if (firstGoodPageIndex > -1 && currentlyFocusedElement !== this.view.element(firstGoodPageIndex)) {
				this.setFocus([firstGoodPageIndex], browserEvent);
			} else {
				this.setFocus([firstPageIndex], browserEvent);
			}
J
Joao Moreno 已提交
1491 1492 1493 1494 1495 1496
		} else {
			const previousScrollTop = scrollTop;
			this.view.setScrollTop(scrollTop - this.view.renderHeight);

			if (this.view.getScrollTop() !== previousScrollTop) {
				// Let the scroll event listener run
J
Joao Moreno 已提交
1497
				setTimeout(() => this.focusPreviousPage(browserEvent, filter), 0);
J
Joao Moreno 已提交
1498 1499 1500 1501
			}
		}
	}

J
Joao Moreno 已提交
1502
	focusLast(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1503
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
1504 1505 1506 1507 1508 1509

		const index = this.findPreviousIndex(this.length - 1, false, filter);

		if (index > -1) {
			this.setFocus([index], browserEvent);
		}
1510 1511
	}

J
Joao Moreno 已提交
1512
	focusFirst(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
B
Benjamin Pasero 已提交
1513 1514 1515 1516
		this.focusNth(0, browserEvent, filter);
	}

	focusNth(n: number, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1517
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
1518

B
Benjamin Pasero 已提交
1519
		const index = this.findNextIndex(n, false, filter);
J
Joao Moreno 已提交
1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559

		if (index > -1) {
			this.setFocus([index], browserEvent);
		}
	}

	private findNextIndex(index: number, loop = false, filter?: (element: T) => boolean): number {
		for (let i = 0; i < this.length; i++) {
			if (index >= this.length && !loop) {
				return -1;
			}

			index = index % this.length;

			if (!filter || filter(this.element(index))) {
				return index;
			}

			index++;
		}

		return -1;
	}

	private findPreviousIndex(index: number, loop = false, filter?: (element: T) => boolean): number {
		for (let i = 0; i < this.length; i++) {
			if (index < 0 && !loop) {
				return -1;
			}

			index = (this.length + (index % this.length)) % this.length;

			if (!filter || filter(this.element(index))) {
				return index;
			}

			index--;
		}

		return -1;
1560 1561
	}

J
Joao Moreno 已提交
1562 1563 1564 1565 1566 1567
	getFocus(): number[] {
		return this.focus.get();
	}

	getFocusedElements(): T[] {
		return this.getFocus().map(i => this.view.element(i));
J
Joao Moreno 已提交
1568 1569
	}

J
Joao Moreno 已提交
1570
	reveal(index: number, relativeTop?: number): void {
J
Joao Moreno 已提交
1571
		if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1572
			throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1573 1574
		}

J
Joao Moreno 已提交
1575 1576 1577 1578 1579 1580
		const scrollTop = this.view.getScrollTop();
		const elementTop = this.view.elementTop(index);
		const elementHeight = this.view.elementHeight(index);

		if (isNumber(relativeTop)) {
			// y = mx + b
J
Joao Moreno 已提交
1581
			const m = elementHeight - this.view.renderHeight;
J
Joao Moreno 已提交
1582
			this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop);
J
Joao Moreno 已提交
1583
		} else {
J
Joao Moreno 已提交
1584
			const viewItemBottom = elementTop + elementHeight;
J
Joao Moreno 已提交
1585
			const wrapperBottom = scrollTop + this.view.renderHeight;
J
Joao Moreno 已提交
1586

1587 1588 1589
			if (elementTop < scrollTop && viewItemBottom >= wrapperBottom) {
				// The element is already overflowing the viewport, no-op
			} else if (elementTop < scrollTop) {
J
Joao Moreno 已提交
1590 1591
				this.view.setScrollTop(elementTop);
			} else if (viewItemBottom >= wrapperBottom) {
J
Joao Moreno 已提交
1592
				this.view.setScrollTop(viewItemBottom - this.view.renderHeight);
J
Joao Moreno 已提交
1593 1594 1595 1596
			}
		}
	}

J
Joao Moreno 已提交
1597 1598 1599 1600 1601
	/**
	 * Returns the relative position of an element rendered in the list.
	 * Returns `null` if the element isn't *entirely* in the visible viewport.
	 */
	getRelativeTop(index: number): number | null {
J
Joao Moreno 已提交
1602
		if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1603
			throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1604 1605
		}

J
Joao Moreno 已提交
1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618
		const scrollTop = this.view.getScrollTop();
		const elementTop = this.view.elementTop(index);
		const elementHeight = this.view.elementHeight(index);

		if (elementTop < scrollTop || elementTop + elementHeight > scrollTop + this.view.renderHeight) {
			return null;
		}

		// y = mx + b
		const m = elementHeight - this.view.renderHeight;
		return Math.abs((scrollTop - elementTop) / m);
	}

1619 1620 1621 1622
	isDOMFocused(): boolean {
		return this.view.domNode === document.activeElement;
	}

1623 1624 1625 1626
	getHTMLElement(): HTMLElement {
		return this.view.domNode;
	}

1627
	open(indexes: number[], browserEvent?: UIEvent): void {
J
Joao Moreno 已提交
1628 1629
		for (const index of indexes) {
			if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1630
				throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1631 1632 1633
			}
		}

J
Joao Moreno 已提交
1634
		this._onDidOpen.fire({ indexes, elements: indexes.map(i => this.view.element(i)), browserEvent });
J
Joao Moreno 已提交
1635 1636
	}

J
Joao Moreno 已提交
1637
	pin(indexes: number[], browserEvent?: UIEvent): void {
J
Joao Moreno 已提交
1638 1639
		for (const index of indexes) {
			if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1640
				throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1641 1642 1643
			}
		}

J
Joao Moreno 已提交
1644
		this._onDidPin.fire({ indexes, elements: indexes.map(i => this.view.element(i)), browserEvent });
J
Joao Moreno 已提交
1645 1646
	}

1647
	style(styles: IListStyles): void {
1648
		this.styleController.style(styles);
1649 1650
	}

1651 1652
	private toListEvent({ indexes, browserEvent }: ITraitChangeEvent) {
		return { indexes, elements: indexes.map(i => this.view.element(i)), browserEvent };
J
Joao Moreno 已提交
1653 1654
	}

J
Joao Moreno 已提交
1655
	private _onFocusChange(): void {
J
João Moreno 已提交
1656
		const focus = this.focus.get();
J
Joao Moreno 已提交
1657 1658 1659 1660 1661 1662
		DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0);
		this.onDidChangeActiveDescendant();
	}

	private onDidChangeActiveDescendant(): void {
		const focus = this.focus.get();
J
João Moreno 已提交
1663 1664

		if (focus.length > 0) {
J
Joao Moreno 已提交
1665 1666 1667 1668 1669 1670 1671
			let id: string | undefined;

			if (this.accessibilityProvider?.getActiveDescendantId) {
				id = this.accessibilityProvider.getActiveDescendantId(this.view.element(focus[0]));
			}

			this.view.domNode.setAttribute('aria-activedescendant', id || this.view.getElementDomId(focus[0]));
J
João Moreno 已提交
1672 1673 1674
		} else {
			this.view.domNode.removeAttribute('aria-activedescendant');
		}
J
Joao Moreno 已提交
1675 1676
	}

1677 1678 1679 1680 1681 1682 1683 1684
	private _onSelectionChange(): void {
		const selection = this.selection.get();

		DOM.toggleClass(this.view.domNode, 'selection-none', selection.length === 0);
		DOM.toggleClass(this.view.domNode, 'selection-single', selection.length === 1);
		DOM.toggleClass(this.view.domNode, 'selection-multiple', selection.length > 1);
	}

J
Joao Moreno 已提交
1685
	dispose(): void {
1686
		this._onDidDispose.fire();
M
Matt Bierner 已提交
1687
		this.disposables.dispose();
1688

J
Joao Moreno 已提交
1689
		this._onDidOpen.dispose();
J
Joao Moreno 已提交
1690
		this._onDidPin.dispose();
1691
		this._onDidDispose.dispose();
J
Joao Moreno 已提交
1692 1693
	}
}