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

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

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

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

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

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

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

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

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

J
Joao Moreno 已提交
59 60 61 62 63 64 65 66
		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 已提交
67

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

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

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

J
Joao Moreno 已提交
76 77 78 79 80 81 82
			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 已提交
83 84
			}
		}
J
Joao Moreno 已提交
85 86

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

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

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

		if (index < 0) {
			return;
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

202 203 204 205 206 207 208 209 210 211
/**
 * 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 已提交
212
		private identityProvider?: IIdentityProvider<T>
213 214 215
	) { }

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

J
Joao Moreno 已提交
220 221
		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);
222 223 224 225 226

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

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

231
class KeyboardController<T> implements IDisposable {
232

M
Matt Bierner 已提交
233
	private readonly disposables = new DisposableStore();
J
Joao Moreno 已提交
234 235 236

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

J
Joao Moreno 已提交
242
		const onKeyDown = Event.chain(domEvent(view.domNode, 'keydown'))
J
Joao Moreno 已提交
243
			.filter(e => !isInputElement(e.target as HTMLElement))
J
Joao Moreno 已提交
244 245 246 247 248 249 250
			.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 已提交
251
		onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables);
J
Joao Moreno 已提交
252 253 254 255

		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 已提交
256 257
	}

258 259 260
	private onEnter(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
261
		this.list.setSelection(this.list.getFocus(), e.browserEvent);
262 263 264 265 266
	}

	private onUpArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
267
		this.list.focusPrevious(1, false, e.browserEvent);
268 269 270 271 272 273 274
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

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

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

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

J
Joao Moreno 已提交
296 297 298
	private onCtrlA(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
299
		this.list.setSelection(range(this.list.length), e.browserEvent);
J
Joao Moreno 已提交
300 301 302 303 304 305
		this.view.domNode.focus();
	}

	private onEscape(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
306
		this.list.setSelection([], e.browserEvent);
J
Joao Moreno 已提交
307 308 309
		this.view.domNode.focus();
	}

J
Joao Moreno 已提交
310
	dispose() {
M
Matt Bierner 已提交
311
		this.disposables.dispose();
J
Joao Moreno 已提交
312 313 314
	}
}

J
Joao Moreno 已提交
315 316 317 318 319
enum TypeLabelControllerState {
	Idle,
	Typing
}

320 321 322 323 324
export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardNavigationDelegate {
	mightProducePrintableCharacter(event: IKeyboardEvent): boolean {
		if (event.ctrlKey || event.metaKey || event.altKey) {
			return false;
		}
J
Joao Moreno 已提交
325

326 327 328 329 330 331
		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);
	}
};
332

333
class TypeLabelController<T> implements IDisposable {
334

335
	private enabled = false;
J
Joao Moreno 已提交
336
	private state: TypeLabelControllerState = TypeLabelControllerState.Idle;
J
Joao Moreno 已提交
337 338 339

	private automaticKeyboardNavigation = true;
	private triggered = false;
340
	private previouslyFocused = -1;
J
Joao Moreno 已提交
341

M
Matt Bierner 已提交
342 343
	private readonly enabledDisposables = new DisposableStore();
	private readonly disposables = new DisposableStore();
J
Joao Moreno 已提交
344 345 346 347

	constructor(
		private list: List<T>,
		private view: ListView<T>,
348 349
		private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>,
		private delegate: IKeyboardNavigationDelegate
J
Joao Moreno 已提交
350
	) {
J
Joao Moreno 已提交
351
		this.updateOptions(list.options);
352 353
	}

J
Joao Moreno 已提交
354
	updateOptions(options: IListOptions<T>): void {
J
Joao Moreno 已提交
355 356 357
		const enableKeyboardNavigation = typeof options.enableKeyboardNavigation === 'undefined' ? true : !!options.enableKeyboardNavigation;

		if (enableKeyboardNavigation) {
358 359 360 361
			this.enable();
		} else {
			this.disable();
		}
J
Joao Moreno 已提交
362 363 364 365 366 367 368 369

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

	toggle(): void {
		this.triggered = !this.triggered;
370 371 372 373 374 375 376 377
	}

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

		const onChar = Event.chain(domEvent(this.view.domNode, 'keydown'))
J
Joao Moreno 已提交
378
			.filter(e => !isInputElement(e.target as HTMLElement))
J
Joao Moreno 已提交
379
			.filter(() => this.automaticKeyboardNavigation || this.triggered)
J
Joao Moreno 已提交
380
			.map(event => new StandardKeyboardEvent(event))
381
			.filter(e => this.delegate.mightProducePrintableCharacter(e))
J
Joao Moreno 已提交
382
			.forEach(e => { e.stopPropagation(); e.preventDefault(); })
J
Joao Moreno 已提交
383 384 385
			.map(event => event.browserEvent.key)
			.event;

J
Joao Moreno 已提交
386 387
		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 已提交
388

389
		onInput(this.onInput, this, this.enabledDisposables);
390
		onClear(this.onClear, this, this.enabledDisposables);
391 392

		this.enabled = true;
J
Joao Moreno 已提交
393
		this.triggered = false;
394 395 396
	}

	private disable(): void {
P
pi1024e 已提交
397 398
		if (!this.enabled) {
			return;
P
pi1024e 已提交
399
		}
P
pi1024e 已提交
400 401 402 403

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

406 407
	private onClear(): void {
		const focus = this.list.getFocus();
408
		if (focus.length > 0 && focus[0] === this.previouslyFocused) {
409
			// List: re-anounce element on typing end since typed keys will interupt aria label of focused element
410
			// Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961
411 412 413 414 415
			const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0]));
			if (ariaLabel) {
				alert(ariaLabel);
			}
		}
416
		this.previouslyFocused = -1;
417 418
	}

J
Joao Moreno 已提交
419 420 421
	private onInput(word: string | null): void {
		if (!word) {
			this.state = TypeLabelControllerState.Idle;
J
Joao Moreno 已提交
422
			this.triggered = false;
J
Joao Moreno 已提交
423 424 425 426 427 428 429 430 431 432
			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 已提交
433
			const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(this.view.element(index));
434
			const labelStr = label && label.toString();
J
Joao Moreno 已提交
435

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

	dispose() {
446
		this.disable();
M
Matt Bierner 已提交
447 448
		this.enabledDisposables.dispose();
		this.disposables.dispose();
J
Joao Moreno 已提交
449 450 451
	}
}

J
Joao Moreno 已提交
452 453
class DOMFocusController<T> implements IDisposable {

M
Matt Bierner 已提交
454
	private readonly disposables = new DisposableStore();
J
Joao Moreno 已提交
455 456 457 458 459

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

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

	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 已提交
480 481 482 483 484

		if (!focusedDomElement) {
			return;
		}

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

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

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

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

	dispose() {
M
Matt Bierner 已提交
502
		this.disposables.dispose();
J
Joao Moreno 已提交
503 504 505
	}
}

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

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

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

T
Tony Xia 已提交
518
const DefaultMultipleSelectionController = {
J
Joao Moreno 已提交
519 520 521 522
	isSelectionSingleChangeEvent,
	isSelectionRangeChangeEvent
};

523
export class MouseController<T> implements IDisposable {
J
Joao Moreno 已提交
524

J
Joao Moreno 已提交
525
	private multipleSelectionSupport: boolean;
J
Joao Moreno 已提交
526
	readonly multipleSelectionController: IMultipleSelectionController<T> | undefined;
J
Joao Moreno 已提交
527
	private mouseSupport: boolean;
M
Matt Bierner 已提交
528
	private readonly disposables = new DisposableStore();
529

530 531
	constructor(protected list: List<T>) {
		this.multipleSelectionSupport = !(list.options.multipleSelectionSupport === false);
J
Joao Moreno 已提交
532 533

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

J
Joao Moreno 已提交
537 538 539 540 541 542 543
		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);
544
			this.disposables.add(Gesture.addTarget(list.getHTMLElement()));
J
Joao Moreno 已提交
545
		}
J
Joao Moreno 已提交
546

547
		list.onMouseClick(this.onPointer, this, this.disposables);
548
		list.onMouseMiddleClick(this.onPointer, this, this.disposables);
549
		list.onTap(this.onPointer, this, this.disposables);
550 551
	}

J
Joao Moreno 已提交
552
	protected isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
J
Joao Moreno 已提交
553 554
		if (this.multipleSelectionController) {
			return this.multipleSelectionController.isSelectionSingleChangeEvent(event);
555 556 557 558 559
		}

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

J
Joao Moreno 已提交
560
	protected isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
J
Joao Moreno 已提交
561 562 563 564
		if (this.multipleSelectionController) {
			return this.multipleSelectionController.isSelectionRangeChangeEvent(event);
		}

565 566 567 568 569 570 571
		return event.browserEvent.shiftKey;
	}

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

J
Joao Moreno 已提交
572
	private onMouseDown(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
J
Joao Moreno 已提交
573
		if (document.activeElement !== e.browserEvent.target) {
574
			this.list.domFocus();
J
Joao Moreno 已提交
575
		}
J
Joao Moreno 已提交
576 577
	}

578
	private onContextMenu(e: IListContextMenuEvent<T>): void {
J
Joao Moreno 已提交
579 580 581
		const focus = typeof e.index === 'undefined' ? [] : [e.index];
		this.list.setFocus(focus, e.browserEvent);
	}
J
Joao Moreno 已提交
582

583
	protected onPointer(e: IListMouseEvent<T>): void {
J
Joao Moreno 已提交
584 585 586 587
		if (!this.mouseSupport) {
			return;
		}

588 589 590 591
		if (isInputElement(e.browserEvent.target as HTMLElement)) {
			return;
		}

J
Joao Moreno 已提交
592
		let reference = this.list.getFocus()[0];
593 594
		const selection = this.list.getSelection();
		reference = reference === undefined ? selection[0] : reference;
J
Joao Moreno 已提交
595

J
Joao Moreno 已提交
596 597 598 599 600 601 602 603
		const focus = e.index;

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

604
		if (this.multipleSelectionSupport && this.isSelectionRangeChangeEvent(e)) {
J
Joao Moreno 已提交
605 606 607
			return this.changeSelection(e, reference);
		}

608
		if (this.multipleSelectionSupport && this.isSelectionChangeEvent(e)) {
J
Joao Moreno 已提交
609 610
			return this.changeSelection(e, reference);
		}
611

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

J
Joao Moreno 已提交
614
		if (!isMouseRightClick(e.browserEvent)) {
615
			this.list.setSelection([focus], e.browserEvent);
616
		}
J
Joao Moreno 已提交
617
	}
J
Joao Moreno 已提交
618

619
	protected onDoubleClick(e: IListMouseEvent<T>): void {
620 621 622 623
		if (isInputElement(e.browserEvent.target as HTMLElement)) {
			return;
		}

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

J
Joao Moreno 已提交
628
		const focus = this.list.getFocus();
629
		this.list.setSelection(focus, e.browserEvent);
J
Joao Moreno 已提交
630
	}
631

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

635
		if (this.isSelectionRangeChangeEvent(e) && reference !== undefined) {
J
Joao Moreno 已提交
636 637
			const min = Math.min(reference, focus);
			const max = Math.max(reference, focus);
J
Joao Moreno 已提交
638
			const rangeSelection = range(min, max + 1);
J
Joao Moreno 已提交
639 640
			const selection = this.list.getSelection();
			const contiguousRange = getContiguousRangeContaining(disjunction(selection, [reference]), reference);
641

J
Joao Moreno 已提交
642 643
			if (contiguousRange.length === 0) {
				return;
644 645
			}

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

649
		} else if (this.isSelectionSingleChangeEvent(e)) {
J
Joao Moreno 已提交
650 651 652
			const selection = this.list.getSelection();
			const newSelection = selection.filter(i => i !== focus);

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

J
Joao Moreno 已提交
655
			if (selection.length === newSelection.length) {
656
				this.list.setSelection([...newSelection, focus], e.browserEvent);
J
Joao Moreno 已提交
657
			} else {
658
				this.list.setSelection(newSelection, e.browserEvent);
J
Joao Moreno 已提交
659 660
			}
		}
661 662 663
	}

	dispose() {
M
Matt Bierner 已提交
664
		this.disposables.dispose();
665 666 667
	}
}

J
Joao Moreno 已提交
668 669 670
export interface IMultipleSelectionController<T> {
	isSelectionSingleChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;
	isSelectionRangeChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;
671 672
}

673 674 675 676
export interface IStyleController {
	style(styles: IListStyles): void;
}

J
João Moreno 已提交
677
export interface IListAccessibilityProvider<T> extends IListViewAccessibilityProvider<T> {
678
	getAriaLabel(element: T): string | null;
679 680
	getWidgetAriaLabel(): string;
	getWidgetRole?(): string;
J
Joao Moreno 已提交
681
	getAriaLevel?(element: T): number | undefined;
J
Joao Moreno 已提交
682 683
	onDidChangeActiveDescendant?: Event<void>;
	getActiveDescendantId?(element: T): string | undefined;
684 685
}

686 687
export class DefaultStyleController implements IStyleController {

688
	constructor(private styleElement: HTMLStyleElement, private selectorSuffix: string) { }
689 690

	style(styles: IListStyles): void {
691
		const suffix = this.selectorSuffix && `.${this.selectorSuffix}`;
692 693
		const content: string[] = [];

694 695 696
		if (styles.listBackground) {
			if (styles.listBackground.isOpaque()) {
				content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`);
J
Joao Moreno 已提交
697
			} else if (!platform.isMacintosh) { // subpixel AA doesn't exist in macOS
698 699 700 701
				console.warn(`List with id '${this.selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`);
			}
		}

702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720
		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 已提交
721
			content.push(`
J
Joao Moreno 已提交
722
				.monaco-drag-image,
J
Joao Moreno 已提交
723 724
				.monaco-list${suffix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }
			`);
725 726 727
		}

		if (styles.listFocusAndSelectionForeground) {
J
Joao Moreno 已提交
728
			content.push(`
J
Joao Moreno 已提交
729
				.monaco-drag-image,
J
Joao Moreno 已提交
730 731
				.monaco-list${suffix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }
			`);
732 733
		}

734 735 736 737 738
		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!
		}

739 740 741 742 743 744 745 746 747 748
		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 已提交
749
			content.push(`.monaco-list${suffix}:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) { background-color:  ${styles.listHoverBackground}; }`);
750 751 752
		}

		if (styles.listHoverForeground) {
J
Joao Moreno 已提交
753
			content.push(`.monaco-list${suffix} .monaco-list-row:hover:not(.selected):not(.focused) { color:  ${styles.listHoverForeground}; }`);
754 755 756 757 758 759 760
		}

		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 已提交
761
			content.push(`
J
Joao Moreno 已提交
762
				.monaco-drag-image,
J
Joao Moreno 已提交
763 764
				.monaco-list${suffix}:focus .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }
			`);
765 766 767 768 769 770 771 772 773 774
		}

		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 已提交
775 776 777
		if (styles.listDropBackground) {
			content.push(`
				.monaco-list${suffix}.drop-target,
J
Joao Moreno 已提交
778
				.monaco-list${suffix} .monaco-list-rows.drop-target,
J
Joao Moreno 已提交
779 780 781
				.monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; }
			`);
		}
J
Joao Moreno 已提交
782

J
Joao Moreno 已提交
783 784
		if (styles.listFilterWidgetBackground) {
			content.push(`.monaco-list-type-filter { background-color: ${styles.listFilterWidgetBackground} }`);
J
Joao Moreno 已提交
785 786
		}

J
Joao Moreno 已提交
787 788
		if (styles.listFilterWidgetOutline) {
			content.push(`.monaco-list-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`);
J
Joao Moreno 已提交
789 790
		}

J
Joao Moreno 已提交
791 792
		if (styles.listFilterWidgetNoMatchesOutline) {
			content.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`);
793 794
		}

J
Joao Moreno 已提交
795
		if (styles.listMatchesShadow) {
J
Joao Moreno 已提交
796
			content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`);
J
Joao Moreno 已提交
797 798
		}

799 800 801 802 803 804 805
		const newStyles = content.join('\n');
		if (newStyles !== this.styleElement.innerHTML) {
			this.styleElement.innerHTML = newStyles;
		}
	}
}

806
export interface IListOptions<T> {
J
Joao Moreno 已提交
807
	readonly identityProvider?: IIdentityProvider<T>;
808
	readonly dnd?: IListDragAndDrop<T>;
809
	readonly enableKeyboardNavigation?: boolean;
J
Joao Moreno 已提交
810
	readonly automaticKeyboardNavigation?: boolean;
J
Joao Moreno 已提交
811
	readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider<T>;
812
	readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate;
J
Joao Moreno 已提交
813 814 815
	readonly keyboardSupport?: boolean;
	readonly multipleSelectionSupport?: boolean;
	readonly multipleSelectionController?: IMultipleSelectionController<T>;
816
	readonly styleController?: (suffix: string) => IStyleController;
J
João Moreno 已提交
817
	readonly accessibilityProvider?: IListAccessibilityProvider<T>;
J
Joao Moreno 已提交
818 819 820 821 822

	// list view options
	readonly useShadows?: boolean;
	readonly verticalScrollMode?: ScrollbarVisibility;
	readonly setRowLineHeight?: boolean;
R
rebornix 已提交
823
	readonly setRowHeight?: boolean;
J
Joao Moreno 已提交
824 825
	readonly supportDynamicHeights?: boolean;
	readonly mouseSupport?: boolean;
826
	readonly horizontalScrolling?: boolean;
827
	readonly additionalScrollHeight?: number;
828
	readonly transformOptimization?: boolean;
J
Joao Moreno 已提交
829 830
}

831
export interface IListStyles {
832
	listBackground?: Color;
833
	listFocusBackground?: Color;
834
	listFocusForeground?: Color;
835 836 837 838 839
	listActiveSelectionBackground?: Color;
	listActiveSelectionForeground?: Color;
	listFocusAndSelectionBackground?: Color;
	listFocusAndSelectionForeground?: Color;
	listInactiveSelectionBackground?: Color;
840
	listInactiveSelectionForeground?: Color;
M
Martin Aeschlimann 已提交
841
	listInactiveFocusBackground?: Color;
842
	listHoverBackground?: Color;
843
	listHoverForeground?: Color;
844 845
	listDropBackground?: Color;
	listFocusOutline?: Color;
846 847 848
	listInactiveFocusOutline?: Color;
	listSelectionOutline?: Color;
	listHoverOutline?: Color;
J
Joao Moreno 已提交
849 850 851
	listFilterWidgetBackground?: Color;
	listFilterWidgetOutline?: Color;
	listFilterWidgetNoMatchesOutline?: Color;
J
Joao Moreno 已提交
852
	listMatchesShadow?: Color;
J
Joao Moreno 已提交
853
	treeIndentGuidesStroke?: Color;
854 855 856
}

const defaultStyles: IListStyles = {
J
Joao Moreno 已提交
857
	listFocusBackground: Color.fromHex('#7FB0D0'),
858 859 860 861 862 863
	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 已提交
864 865
	listDropBackground: Color.fromHex('#383B3D'),
	treeIndentGuidesStroke: Color.fromHex('#a9a9a9')
866 867
};

J
João Moreno 已提交
868
const DefaultOptions: IListOptions<any> = {
869
	keyboardSupport: true,
I
isidor 已提交
870
	mouseSupport: true,
J
Joao Moreno 已提交
871 872 873 874 875 876
	multipleSelectionSupport: true,
	dnd: {
		getDragURI() { return null; },
		onDragStart(): void { },
		onDragOver() { return false; },
		drop() { }
J
João Moreno 已提交
877
	}
878
};
J
Joao Moreno 已提交
879

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

882 883 884 885 886 887 888
function getContiguousRangeContaining(range: number[], value: number): number[] {
	const index = range.indexOf(value);

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

M
Matt Bierner 已提交
889
	const result: number[] = [];
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905
	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 已提交
906
 * between them (OR).
907 908
 */
function disjunction(one: number[], other: number[]): number[] {
M
Matt Bierner 已提交
909
	const result: number[] = [];
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936
	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 已提交
937
	const result: number[] = [];
938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958
	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;
}

959 960
const numericSort = (a: number, b: number) => a - b;

J
Joao Moreno 已提交
961
class PipelineRenderer<T> implements IListRenderer<T, any> {
J
Joao Moreno 已提交
962 963 964

	constructor(
		private _templateId: string,
J
Joao Moreno 已提交
965
		private renderers: IListRenderer<any /* TODO@joao */, any>[]
J
Joao Moreno 已提交
966 967 968 969 970 971 972 973 974 975
	) { }

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

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

976
	renderElement(element: T, index: number, templateData: any[], height: number | undefined): void {
J
Joao Moreno 已提交
977 978 979
		let i = 0;

		for (const renderer of this.renderers) {
980
			renderer.renderElement(element, index, templateData[i++], height);
J
Joao Moreno 已提交
981
		}
J
Joao Moreno 已提交
982 983
	}

984
	disposeElement(element: T, index: number, templateData: any[], height: number | undefined): void {
J
Joao Moreno 已提交
985 986 987
		let i = 0;

		for (const renderer of this.renderers) {
J
Joao Moreno 已提交
988
			if (renderer.disposeElement) {
989
				renderer.disposeElement(element, index, templateData[i], height);
J
Joao Moreno 已提交
990
			}
J
fix npe  
Joao Moreno 已提交
991 992

			i += 1;
J
Joao Moreno 已提交
993 994 995
		}
	}

J
Joao Moreno 已提交
996
	disposeTemplate(templateData: any[]): void {
J
Joao Moreno 已提交
997 998 999
		let i = 0;

		for (const renderer of this.renderers) {
J
Joao Moreno 已提交
1000
			renderer.disposeTemplate(templateData[i++]);
J
Joao Moreno 已提交
1001
		}
J
Joao Moreno 已提交
1002 1003 1004
	}
}

1005 1006 1007 1008
class AccessibiltyRenderer<T> implements IListRenderer<T, HTMLElement> {

	templateId: string = 'a18n';

J
João Moreno 已提交
1009
	constructor(private accessibilityProvider: IListAccessibilityProvider<T>) { }
1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022

	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 已提交
1023 1024 1025 1026 1027 1028 1029 1030

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

		if (typeof ariaLevel === 'number') {
			container.setAttribute('aria-level', `${ariaLevel}`);
		} else {
			container.removeAttribute('aria-level');
		}
1031 1032 1033 1034 1035 1036 1037
	}

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

J
Joao Moreno 已提交
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
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 已提交
1052
	getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined {
J
Joao Moreno 已提交
1053
		if (this.dnd.getDragLabel) {
J
Joao Moreno 已提交
1054
			return this.dnd.getDragLabel(elements, originalEvent);
J
Joao Moreno 已提交
1055 1056 1057
		}

		return undefined;
J
Joao Moreno 已提交
1058 1059 1060
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
J
Joao Moreno 已提交
1061 1062 1063
		if (this.dnd.onDragStart) {
			this.dnd.onDragStart(data, originalEvent);
		}
J
Joao Moreno 已提交
1064 1065 1066 1067 1068 1069
	}

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

J
Joao Moreno 已提交
1070 1071 1072 1073 1074 1075
	onDragEnd(originalEvent: DragEvent): void {
		if (this.dnd.onDragEnd) {
			this.dnd.onDragEnd(originalEvent);
		}
	}

J
Joao Moreno 已提交
1076 1077 1078 1079 1080
	drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
		this.dnd.drop(data, targetElement, targetIndex, originalEvent);
	}
}

J
João Moreno 已提交
1081
export interface IListOptionsUpdate extends IListViewOptionsUpdate {
1082
	readonly enableKeyboardNavigation?: boolean;
J
Joao Moreno 已提交
1083
	readonly automaticKeyboardNavigation?: boolean;
1084 1085
}

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

A
Alex Dima 已提交
1088 1089
	private focus: Trait<T>;
	private selection: Trait<T>;
J
Joao Moreno 已提交
1090
	private eventBufferer = new EventBufferer();
R
rebornix 已提交
1091
	protected view: ListView<T>;
1092
	private spliceable: ISpliceable<T>;
1093
	private styleController: IStyleController;
J
Joao Moreno 已提交
1094
	private typeLabelController?: TypeLabelController<T>;
J
João Moreno 已提交
1095
	private accessibilityProvider?: IListAccessibilityProvider<T>;
1096
	private _ariaLabel: string = '';
1097

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

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

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

J
Joao Moreno 已提交
1108
	get domId(): string { return this.view.domId; }
J
Joao Moreno 已提交
1109
	get onDidScroll(): Event<ScrollEvent> { return this.view.onDidScroll; }
J
Joao Moreno 已提交
1110 1111
	get onMouseClick(): Event<IListMouseEvent<T>> { return this.view.onMouseClick; }
	get onMouseDblClick(): Event<IListMouseEvent<T>> { return this.view.onMouseDblClick; }
1112
	get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return this.view.onMouseMiddleClick; }
J
Joao Moreno 已提交
1113 1114 1115 1116 1117 1118 1119 1120
	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 已提交
1121 1122
	private didJustPressContextMenuKey: boolean = false;
	@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
J
Joao Moreno 已提交
1123
		const fromKeydown = Event.chain(domEvent(this.view.domNode, 'keydown'))
J
Joao Moreno 已提交
1124 1125 1126
			.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; })
1127
			.event as Event<any>;
J
Joao Moreno 已提交
1128

J
Joao Moreno 已提交
1129
		const fromKeyup = Event.chain(domEvent(this.view.domNode, 'keyup'))
J
Joao Moreno 已提交
1130 1131 1132 1133 1134
			.filter(() => {
				const didJustPressContextMenuKey = this.didJustPressContextMenuKey;
				this.didJustPressContextMenuKey = false;
				return didJustPressContextMenuKey;
			})
1135
			.filter(() => this.getFocus().length > 0 && !!this.view.domElement(this.getFocus()[0]))
J
Joao Moreno 已提交
1136 1137 1138
			.map(browserEvent => {
				const index = this.getFocus()[0];
				const element = this.view.element(index);
1139
				const anchor = this.view.domElement(index) as HTMLElement;
J
Joao Moreno 已提交
1140 1141 1142 1143
				return { index, element, anchor, browserEvent };
			})
			.event;

J
Joao Moreno 已提交
1144
		const fromMouse = Event.chain(this.view.onContextMenu)
J
Joao Moreno 已提交
1145 1146 1147 1148
			.filter(() => !this.didJustPressContextMenuKey)
			.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent }))
			.event;

J
Joao Moreno 已提交
1149
		return Event.any<IListContextMenuEvent<T>>(fromKeydown, fromKeyup, fromMouse);
J
Joao Moreno 已提交
1150 1151
	}

J
Joao Moreno 已提交
1152 1153 1154
	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 已提交
1155

1156 1157
	readonly onDidFocus: Event<void>;
	readonly onDidBlur: Event<void>;
1158

1159
	private readonly _onDidDispose = new Emitter<void>();
1160
	readonly onDidDispose: Event<void> = this._onDidDispose.event;
1161

J
Joao Moreno 已提交
1162
	constructor(
J
Joao Moreno 已提交
1163
		private user: string,
J
Joao Moreno 已提交
1164
		container: HTMLElement,
J
Joao Moreno 已提交
1165
		virtualDelegate: IListVirtualDelegate<T>,
J
Joao Moreno 已提交
1166
		renderers: IListRenderer<any /* TODO@joao */, any>[],
1167
		private _options: IListOptions<T> = DefaultOptions
J
Joao Moreno 已提交
1168
	) {
1169 1170
		const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list';
		this.selection = new SelectionTrait(role !== 'listbox');
1171
		this.focus = new Trait('focused');
1172

1173
		mixin(_options, defaultStyles, false);
J
Joao Moreno 已提交
1174

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

J
Joao Moreno 已提交
1177 1178 1179 1180 1181 1182 1183 1184
		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);
			}
1185 1186 1187
		}

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

J
Joao Moreno 已提交
1189
		const viewOptions: IListViewOptions<T> = {
1190 1191
			..._options,
			dnd: _options.dnd && new ListViewDragAndDrop(this, _options.dnd)
J
Joao Moreno 已提交
1192 1193 1194
		};

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

1197 1198 1199 1200 1201 1202
		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);
		}
1203

1204
		this.spliceable = new CombinedSpliceable([
1205 1206
			new TraitSpliceable(this.focus, this.view, _options.identityProvider),
			new TraitSpliceable(this.selection, this.view, _options.identityProvider),
1207 1208 1209
			this.view
		]);

M
Matt Bierner 已提交
1210 1211 1212 1213
		this.disposables.add(this.focus);
		this.disposables.add(this.selection);
		this.disposables.add(this.view);
		this.disposables.add(this._onDidDispose);
1214

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

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

1220 1221
		if (typeof _options.keyboardSupport !== 'boolean' || _options.keyboardSupport) {
			const controller = new KeyboardController(this, this.view, _options);
M
Matt Bierner 已提交
1222
			this.disposables.add(controller);
1223 1224
		}

1225
		if (_options.keyboardNavigationLabelProvider) {
1226 1227
			const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;
			this.typeLabelController = new TypeLabelController(this, this.view, _options.keyboardNavigationLabelProvider, delegate);
M
Matt Bierner 已提交
1228
			this.disposables.add(this.typeLabelController);
J
Joao Moreno 已提交
1229 1230
		}

M
Matt Bierner 已提交
1231
		this.disposables.add(this.createMouseController(_options));
1232

1233 1234
		this.onDidChangeFocus(this._onFocusChange, this, this.disposables);
		this.onDidChangeSelection(this._onSelectionChange, this, this.disposables);
J
João Moreno 已提交
1235

1236 1237
		if (this.accessibilityProvider) {
			this.ariaLabel = this.accessibilityProvider.getWidgetAriaLabel();
J
João Moreno 已提交
1238
		}
1239 1240 1241
		if (_options.multipleSelectionSupport) {
			this.view.domNode.setAttribute('aria-multiselectable', 'true');
		}
1242 1243
	}

1244 1245 1246 1247
	protected createMouseController(options: IListOptions<T>): MouseController<T> {
		return new MouseController(this);
	}

1248 1249
	updateOptions(optionsUpdate: IListOptionsUpdate = {}): void {
		this._options = { ...this._options, ...optionsUpdate };
J
Joao Moreno 已提交
1250 1251 1252 1253

		if (this.typeLabelController) {
			this.typeLabelController.updateOptions(this._options);
		}
1254

J
João Moreno 已提交
1255
		this.view.updateOptions(optionsUpdate);
1256 1257 1258 1259
	}

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

J
Joao Moreno 已提交
1262
	splice(start: number, deleteCount: number, elements: T[] = []): void {
J
Joao Moreno 已提交
1263
		if (start < 0 || start > this.view.length) {
J
Joao Moreno 已提交
1264
			throw new ListError(this.user, `Invalid start index: ${start}`);
J
Joao Moreno 已提交
1265 1266 1267
		}

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

J
Joao Moreno 已提交
1271 1272 1273 1274
		if (deleteCount === 0 && elements.length === 0) {
			return;
		}

1275
		this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements));
J
Joao Moreno 已提交
1276 1277
	}

J
Joao Moreno 已提交
1278 1279 1280 1281
	updateWidth(index: number): void {
		this.view.updateWidth(index);
	}

1282
	updateElementHeight(index: number, size: number): void {
1283
		this.view.updateElementHeight(index, size, null);
1284 1285
	}

I
isidor 已提交
1286 1287 1288 1289
	rerender(): void {
		this.view.rerender();
	}

J
Joao Moreno 已提交
1290 1291 1292 1293
	element(index: number): T {
		return this.view.element(index);
	}

J
Joao Moreno 已提交
1294 1295 1296 1297
	get length(): number {
		return this.view.length;
	}

J
Joao Moreno 已提交
1298
	get contentHeight(): number {
J
Joao Moreno 已提交
1299 1300 1301 1302 1303
		return this.view.contentHeight;
	}

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

J
Joao Moreno 已提交
1306 1307 1308 1309
	get scrollTop(): number {
		return this.view.getScrollTop();
	}

J
Joao Moreno 已提交
1310 1311 1312 1313
	set scrollTop(scrollTop: number) {
		this.view.setScrollTop(scrollTop);
	}

I
isidor 已提交
1314 1315 1316 1317 1318
	get scrollLeft(): number {
		return this.view.getScrollLeft();
	}

	set scrollLeft(scrollLeft: number) {
J
Joao Moreno 已提交
1319
		this.view.setScrollLeft(scrollLeft);
I
isidor 已提交
1320 1321
	}

J
Joao Moreno 已提交
1322 1323 1324 1325
	get scrollHeight(): number {
		return this.view.scrollHeight;
	}

I
isidor 已提交
1326 1327 1328 1329
	get renderHeight(): number {
		return this.view.renderHeight;
	}

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

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

1338 1339 1340 1341 1342 1343
	get ariaLabel(): string {
		return this._ariaLabel;
	}

	set ariaLabel(value: string) {
		this._ariaLabel = value;
1344
		this.view.domNode.setAttribute('aria-label', value);
1345 1346
	}

J
Joao Moreno 已提交
1347 1348 1349 1350
	domFocus(): void {
		this.view.domNode.focus();
	}

1351 1352
	layout(height?: number, width?: number): void {
		this.view.layout(height, width);
J
Joao Moreno 已提交
1353 1354
	}

J
Joao Moreno 已提交
1355 1356 1357 1358 1359 1360
	toggleKeyboardNavigation(): void {
		if (this.typeLabelController) {
			this.typeLabelController.toggle();
		}
	}

1361
	setSelection(indexes: number[], browserEvent?: UIEvent): void {
J
Joao Moreno 已提交
1362 1363
		for (const index of indexes) {
			if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1364
				throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1365 1366 1367
			}
		}

1368
		this.selection.set(indexes, browserEvent);
J
Joao Moreno 已提交
1369 1370
	}

J
Joao Moreno 已提交
1371 1372 1373 1374
	getSelection(): number[] {
		return this.selection.get();
	}

1375 1376 1377 1378
	getSelectedElements(): T[] {
		return this.getSelection().map(i => this.view.element(i));
	}

1379
	setFocus(indexes: number[], browserEvent?: UIEvent): void {
J
Joao Moreno 已提交
1380 1381
		for (const index of indexes) {
			if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1382
				throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1383 1384 1385
			}
		}

1386
		this.focus.set(indexes, browserEvent);
J
Joao Moreno 已提交
1387 1388
	}

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

J
Joao Moreno 已提交
1392
		const focus = this.focus.get();
J
Joao Moreno 已提交
1393 1394 1395 1396 1397
		const index = this.findNextIndex(focus.length > 0 ? focus[0] + n : 0, loop, filter);

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

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

J
Joao Moreno 已提交
1403
		const focus = this.focus.get();
J
Joao Moreno 已提交
1404 1405 1406 1407 1408
		const index = this.findPreviousIndex(focus.length > 0 ? focus[0] - n : 0, loop, filter);

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

J
Joao Moreno 已提交
1411
	focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
J
Joao Moreno 已提交
1412 1413 1414
		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 已提交
1415
		const currentlyFocusedElement = this.getFocusedElements()[0];
J
Joao Moreno 已提交
1416 1417

		if (currentlyFocusedElement !== lastPageElement) {
J
Joao Moreno 已提交
1418 1419 1420 1421 1422 1423 1424
			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 已提交
1425 1426
		} else {
			const previousScrollTop = this.view.getScrollTop();
J
Joao Moreno 已提交
1427
			this.view.setScrollTop(previousScrollTop + this.view.renderHeight - this.view.elementHeight(lastPageIndex));
J
Joao Moreno 已提交
1428 1429 1430

			if (this.view.getScrollTop() !== previousScrollTop) {
				// Let the scroll event listener run
J
Joao Moreno 已提交
1431
				setTimeout(() => this.focusNextPage(browserEvent, filter), 0);
J
Joao Moreno 已提交
1432 1433 1434 1435
			}
		}
	}

J
Joao Moreno 已提交
1436
	focusPreviousPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
J
Johannes Rieken 已提交
1437
		let firstPageIndex: number;
J
Joao Moreno 已提交
1438 1439 1440 1441 1442 1443 1444 1445 1446
		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 已提交
1447
		const currentlyFocusedElement = this.getFocusedElements()[0];
J
Joao Moreno 已提交
1448 1449

		if (currentlyFocusedElement !== firstPageElement) {
J
Joao Moreno 已提交
1450 1451 1452 1453 1454 1455 1456
			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 已提交
1457 1458 1459 1460 1461 1462
		} 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 已提交
1463
				setTimeout(() => this.focusPreviousPage(browserEvent, filter), 0);
J
Joao Moreno 已提交
1464 1465 1466 1467
			}
		}
	}

J
Joao Moreno 已提交
1468
	focusLast(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1469
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
1470 1471 1472 1473 1474 1475

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

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

J
Joao Moreno 已提交
1478
	focusFirst(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
B
Benjamin Pasero 已提交
1479 1480 1481 1482
		this.focusNth(0, browserEvent, filter);
	}

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

B
Benjamin Pasero 已提交
1485
		const index = this.findNextIndex(n, false, filter);
J
Joao Moreno 已提交
1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525

		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;
1526 1527
	}

J
Joao Moreno 已提交
1528 1529 1530 1531 1532 1533
	getFocus(): number[] {
		return this.focus.get();
	}

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

J
Joao Moreno 已提交
1536
	reveal(index: number, relativeTop?: number): void {
J
Joao Moreno 已提交
1537
		if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1538
			throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1539 1540
		}

J
Joao Moreno 已提交
1541 1542 1543 1544 1545 1546
		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 已提交
1547
			const m = elementHeight - this.view.renderHeight;
J
Joao Moreno 已提交
1548
			this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop);
J
Joao Moreno 已提交
1549
		} else {
J
Joao Moreno 已提交
1550
			const viewItemBottom = elementTop + elementHeight;
J
Joao Moreno 已提交
1551
			const wrapperBottom = scrollTop + this.view.renderHeight;
J
Joao Moreno 已提交
1552

1553 1554 1555
			if (elementTop < scrollTop && viewItemBottom >= wrapperBottom) {
				// The element is already overflowing the viewport, no-op
			} else if (elementTop < scrollTop) {
J
Joao Moreno 已提交
1556 1557
				this.view.setScrollTop(elementTop);
			} else if (viewItemBottom >= wrapperBottom) {
J
Joao Moreno 已提交
1558
				this.view.setScrollTop(viewItemBottom - this.view.renderHeight);
J
Joao Moreno 已提交
1559 1560 1561 1562
			}
		}
	}

J
Joao Moreno 已提交
1563 1564 1565 1566 1567
	/**
	 * 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 已提交
1568
		if (index < 0 || index >= this.length) {
J
Joao Moreno 已提交
1569
			throw new ListError(this.user, `Invalid index ${index}`);
J
Joao Moreno 已提交
1570 1571
		}

J
Joao Moreno 已提交
1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584
		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);
	}

1585 1586 1587 1588
	isDOMFocused(): boolean {
		return this.view.domNode === document.activeElement;
	}

1589 1590 1591 1592
	getHTMLElement(): HTMLElement {
		return this.view.domNode;
	}

1593
	style(styles: IListStyles): void {
1594
		this.styleController.style(styles);
1595 1596
	}

1597 1598
	private toListEvent({ indexes, browserEvent }: ITraitChangeEvent) {
		return { indexes, elements: indexes.map(i => this.view.element(i)), browserEvent };
J
Joao Moreno 已提交
1599 1600
	}

J
Joao Moreno 已提交
1601
	private _onFocusChange(): void {
J
João Moreno 已提交
1602
		const focus = this.focus.get();
J
Joao Moreno 已提交
1603 1604 1605 1606 1607 1608
		DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0);
		this.onDidChangeActiveDescendant();
	}

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

		if (focus.length > 0) {
J
Joao Moreno 已提交
1611 1612 1613 1614 1615 1616 1617
			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 已提交
1618 1619 1620
		} else {
			this.view.domNode.removeAttribute('aria-activedescendant');
		}
J
Joao Moreno 已提交
1621 1622
	}

1623 1624 1625 1626 1627 1628 1629 1630
	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 已提交
1631
	dispose(): void {
1632
		this._onDidDispose.fire();
M
Matt Bierner 已提交
1633
		this.disposables.dispose();
1634 1635

		this._onDidDispose.dispose();
J
Joao Moreno 已提交
1636 1637
	}
}