Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/list/listWidget.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IDragAndDropData } from '../../dnd.js';
7
import { Dimension, EventHelper, getActiveElement, getWindow, isActiveElement, isEditableElement, isHTMLElement, isMouseEvent } from '../../dom.js';
8
import { createStyleSheet } from '../../domStylesheets.js';
9
import { asCssValueWithDefault } from '../../cssValue.js';
10
import { DomEmitter } from '../../event.js';
11
import { IKeyboardEvent, StandardKeyboardEvent } from '../../keyboardEvent.js';
12
import { Gesture } from '../../touch.js';
13
import { alert, AriaRole } from '../aria/aria.js';
14
import { CombinedSpliceable } from './splice.js';
15
import { ScrollableElementChangeOptions } from '../scrollbar/scrollableElementOptions.js';
16
import { binarySearch, range } from '../../../common/arrays.js';
17
import { timeout } from '../../../common/async.js';
18
import { Color } from '../../../common/color.js';
19
import { memoize } from '../../../common/decorators.js';
20
import { Emitter, Event, EventBufferer } from '../../../common/event.js';
21
import { matchesFuzzy2, matchesPrefix } from '../../../common/filters.js';
22
import { KeyCode } from '../../../common/keyCodes.js';
23
import { DisposableStore, dispose, IDisposable } from '../../../common/lifecycle.js';
24
import { clamp } from '../../../common/numbers.js';
25
import * as platform from '../../../common/platform.js';
26
import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';
27
import { ISpliceable } from '../../../common/sequence.js';
28
import { isNumber } from '../../../common/types.js';
29
import './list.css';
30
import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListEvent, IListGestureEvent, IListMouseEvent, IListElementRenderDetails, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListError } from './list.js';
31
import { IListView, IListViewAccessibilityProvider, IListViewDragAndDrop, IListViewOptions, IListViewOptionsUpdate, ListViewTargetSector, ListView } from './listView.js';
32
import { IMouseWheelEvent, StandardMouseEvent } from '../../mouseEvent.js';
33
import { autorun, constObservable, IObservable } from '../../../common/observable.js';
34
35
interface ITraitChangeEvent {
36
indexes: number[];
37
browserEvent?: UIEvent;
38
}
39
40
type ITraitTemplateData = HTMLElement;
41
42
type IAccessibilityTemplateData = {
43
container: HTMLElement;
44
disposables: DisposableStore;
45
};
46
47
interface IRenderedContainer {
48
templateData: ITraitTemplateData;
49
index: number;
50
}
51
52
class TraitRenderer<T> implements IListRenderer<T, ITraitTemplateData> {
53
private renderedElements: IRenderedContainer[] = [];
54
55
constructor(private trait: Trait<T>) { }
56
57
get templateId(): string {
58
return `template:${this.trait.name}`;
59
}
60
61
renderTemplate(container: HTMLElement): ITraitTemplateData {
62
return container;
63
}
64
65
renderElement(element: T, index: number, templateData: ITraitTemplateData): void {
66
const renderedElementIndex = this.renderedElements.findIndex(el => el.templateData === templateData);
67
68
if (renderedElementIndex >= 0) {
69
const rendered = this.renderedElements[renderedElementIndex];
70
this.trait.unrender(templateData);
71
rendered.index = index;
72
} else {
73
const rendered = { index, templateData };
74
this.renderedElements.push(rendered);
75
}
76
77
this.trait.renderIndex(index, templateData);
78
}
79
80
splice(start: number, deleteCount: number, insertCount: number): void {
81
const rendered: IRenderedContainer[] = [];
82
83
for (const renderedElement of this.renderedElements) {
84
85
if (renderedElement.index < start) {
86
rendered.push(renderedElement);
87
} else if (renderedElement.index >= start + deleteCount) {
88
rendered.push({
89
index: renderedElement.index + insertCount - deleteCount,
90
templateData: renderedElement.templateData
91
});
92
}
93
}
94
95
this.renderedElements = rendered;
96
}
97
98
renderIndexes(indexes: number[]): void {
99
for (const { index, templateData } of this.renderedElements) {
100
if (indexes.indexOf(index) > -1) {
101
this.trait.renderIndex(index, templateData);
102
}
103
}
104
}
105
106
disposeTemplate(templateData: ITraitTemplateData): void {
107
const index = this.renderedElements.findIndex(el => el.templateData === templateData);
108
109
if (index < 0) {
110
return;
111
}
112
113
this.renderedElements.splice(index, 1);
114
}
115
}
116
117
class Trait<T> implements ISpliceable<boolean>, IDisposable {
118
119
protected indexes: number[] = [];
120
protected sortedIndexes: number[] = [];
121
122
private readonly _onChange = new Emitter<ITraitChangeEvent>();
123
get onChange(): Event<ITraitChangeEvent> { return this._onChange.event; }
124
125
get name(): string { return this._trait; }
126
127
@memoize
128
get renderer(): TraitRenderer<T> {
129
return new TraitRenderer<T>(this);
130
}
131
132
constructor(private _trait: string) { }
133
134
splice(start: number, deleteCount: number, elements: boolean[]): void {
135
const diff = elements.length - deleteCount;
136
const end = start + deleteCount;
137
const sortedIndexes: number[] = [];
138
let i = 0;
139
140
while (i < this.sortedIndexes.length && this.sortedIndexes[i] < start) {
141
sortedIndexes.push(this.sortedIndexes[i++]);
142
}
143
144
for (let j = 0; j < elements.length; j++) {
145
if (elements[j]) {
146
sortedIndexes.push(j + start);
147
}
148
}
149
150
while (i < this.sortedIndexes.length && this.sortedIndexes[i] >= end) {
151
sortedIndexes.push(this.sortedIndexes[i++] + diff);
152
}
153
154
this.renderer.splice(start, deleteCount, elements.length);
155
this._set(sortedIndexes, sortedIndexes);
156
}
157
158
renderIndex(index: number, container: HTMLElement): void {
159
container.classList.toggle(this._trait, this.contains(index));
160
}
161
162
unrender(container: HTMLElement): void {
163
container.classList.remove(this._trait);
164
}
165
166
/**
167
* Sets the indexes which should have this trait.
168
*
169
* @param indexes Indexes which should have this trait.
170
* @return The old indexes which had this trait.
171
*/
172
set(indexes: number[], browserEvent?: UIEvent): number[] {
173
return this._set(indexes, [...indexes].sort(numericSort), browserEvent);
174
}
175
176
private _set(indexes: number[], sortedIndexes: number[], browserEvent?: UIEvent): number[] {
177
const result = this.indexes;
178
const sortedResult = this.sortedIndexes;
179
180
this.indexes = indexes;
181
this.sortedIndexes = sortedIndexes;
182
183
const toRender = disjunction(sortedResult, indexes);
184
this.renderer.renderIndexes(toRender);
185
186
this._onChange.fire({ indexes, browserEvent });
187
return result;
188
}
189
190
get(): number[] {
191
return this.indexes;
192
}
193
194
contains(index: number): boolean {
195
return binarySearch(this.sortedIndexes, index, numericSort) >= 0;
196
}
197
198
dispose() {
199
dispose(this._onChange);
200
}
201
}
202
203
class SelectionTrait<T> extends Trait<T> {
204
205
constructor(private setAriaSelected: boolean) {
206
super('selected');
207
}
208
209
override renderIndex(index: number, container: HTMLElement): void {
210
super.renderIndex(index, container);
211
212
if (this.setAriaSelected) {
213
if (this.contains(index)) {
214
container.setAttribute('aria-selected', 'true');
215
} else {
216
container.setAttribute('aria-selected', 'false');
217
}
218
}
219
}
220
}
221
222
/**
223
* The TraitSpliceable is used as a util class to be able
224
* to preserve traits across splice calls, given an identity
225
* provider.
226
*/
227
class TraitSpliceable<T> implements ISpliceable<T> {
228
229
constructor(
230
private trait: Trait<T>,
231
private view: IListView<T>,
232
private identityProvider?: IIdentityProvider<T>
233
) { }
234
235
splice(start: number, deleteCount: number, elements: T[]): void {
236
if (!this.identityProvider) {
237
return this.trait.splice(start, deleteCount, new Array(elements.length).fill(false));
238
}
239
240
const pastElementsWithTrait = this.trait.get().map(i => this.identityProvider!.getId(this.view.element(i)).toString());
241
if (pastElementsWithTrait.length === 0) {
242
return this.trait.splice(start, deleteCount, new Array(elements.length).fill(false));
243
}
244
245
const pastElementsWithTraitSet = new Set(pastElementsWithTrait);
246
const elementsWithTrait = elements.map(e => pastElementsWithTraitSet.has(this.identityProvider!.getId(e).toString()));
247
this.trait.splice(start, deleteCount, elementsWithTrait);
248
}
249
}
250
251
function isListElementDescendantOfClass(e: HTMLElement, className: string): boolean {
252
if (e.classList.contains(className)) {
253
return true;
254
}
255
256
if (e.classList.contains('monaco-list')) {
257
return false;
258
}
259
260
if (!e.parentElement) {
261
return false;
262
}
263
264
return isListElementDescendantOfClass(e.parentElement, className);
265
}
266
267
export function isMonacoEditor(e: HTMLElement): boolean {
268
return isListElementDescendantOfClass(e, 'monaco-editor');
269
}
270
271
export function isMonacoCustomToggle(e: HTMLElement): boolean {
272
return isListElementDescendantOfClass(e, 'monaco-custom-toggle');
273
}
274
275
export function isActionItem(e: HTMLElement): boolean {
276
return isListElementDescendantOfClass(e, 'action-item');
277
}
278
279
export function isMonacoTwistie(e: HTMLElement): boolean {
280
return isListElementDescendantOfClass(e, 'monaco-tl-twistie');
281
}
282
283
export function isStickyScrollElement(e: HTMLElement): boolean {
284
return isListElementDescendantOfClass(e, 'monaco-tree-sticky-row');
285
}
286
287
export function isStickyScrollContainer(e: HTMLElement): boolean {
288
return e.classList.contains('monaco-tree-sticky-container');
289
}
290
291
export function isButton(e: HTMLElement): boolean {
292
if ((e.tagName === 'A' && e.classList.contains('monaco-button')) ||
293
(e.tagName === 'DIV' && e.classList.contains('monaco-button-dropdown'))) {
294
return true;
295
}
296
297
if (e.classList.contains('monaco-list')) {
298
return false;
299
}
300
301
if (!e.parentElement) {
302
return false;
303
}
304
305
return isButton(e.parentElement);
306
}
307
308
class KeyboardController<T> implements IDisposable {
309
310
private readonly disposables = new DisposableStore();
311
private readonly multipleSelectionDisposables = new DisposableStore();
312
private multipleSelectionSupport: boolean | undefined;
313
314
@memoize
315
private get onKeyDown(): Event<StandardKeyboardEvent> {
316
return Event.chain(
317
this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>
318
$.filter(e => !isEditableElement(e.target as HTMLElement))
319
.map(e => new StandardKeyboardEvent(e))
320
);
321
}
322
323
constructor(
324
private list: List<T>,
325
private view: IListView<T>,
326
options: IListOptions<T>
327
) {
328
this.multipleSelectionSupport = options.multipleSelectionSupport;
329
this.disposables.add(this.onKeyDown(e => {
330
switch (e.keyCode) {
331
case KeyCode.Enter:
332
return this.onEnter(e);
333
case KeyCode.UpArrow:
334
return this.onUpArrow(e);
335
case KeyCode.DownArrow:
336
return this.onDownArrow(e);
337
case KeyCode.PageUp:
338
return this.onPageUpArrow(e);
339
case KeyCode.PageDown:
340
return this.onPageDownArrow(e);
341
case KeyCode.Escape:
342
return this.onEscape(e);
343
case KeyCode.KeyA:
344
if (this.multipleSelectionSupport && (platform.isMacintosh ? e.metaKey : e.ctrlKey)) {
345
this.onCtrlA(e);
346
}
347
}
348
}));
349
}
350
351
updateOptions(optionsUpdate: IListOptionsUpdate): void {
352
if (optionsUpdate.multipleSelectionSupport !== undefined) {
353
this.multipleSelectionSupport = optionsUpdate.multipleSelectionSupport;
354
}
355
}
356
357
private onEnter(e: StandardKeyboardEvent): void {
358
e.preventDefault();
359
e.stopPropagation();
360
this.list.setSelection(this.list.getFocus(), e.browserEvent);
361
}
362
363
private onUpArrow(e: StandardKeyboardEvent): void {
364
e.preventDefault();
365
e.stopPropagation();
366
this.list.focusPrevious(1, false, e.browserEvent);
367
const el = this.list.getFocus()[0];
368
this.list.setAnchor(el);
369
this.list.reveal(el);
370
this.view.domNode.focus();
371
}
372
373
private onDownArrow(e: StandardKeyboardEvent): void {
374
e.preventDefault();
375
e.stopPropagation();
376
this.list.focusNext(1, false, e.browserEvent);
377
const el = this.list.getFocus()[0];
378
this.list.setAnchor(el);
379
this.list.reveal(el);
380
this.view.domNode.focus();
381
}
382
383
private onPageUpArrow(e: StandardKeyboardEvent): void {
384
e.preventDefault();
385
e.stopPropagation();
386
this.list.focusPreviousPage(e.browserEvent);
387
const el = this.list.getFocus()[0];
388
this.list.setAnchor(el);
389
this.list.reveal(el);
390
this.view.domNode.focus();
391
}
392
393
private onPageDownArrow(e: StandardKeyboardEvent): void {
394
e.preventDefault();
395
e.stopPropagation();
396
this.list.focusNextPage(e.browserEvent);
397
const el = this.list.getFocus()[0];
398
this.list.setAnchor(el);
399
this.list.reveal(el);
400
this.view.domNode.focus();
401
}
402
403
private onCtrlA(e: StandardKeyboardEvent): void {
404
e.preventDefault();
405
e.stopPropagation();
406
this.list.setSelection(range(this.list.length), e.browserEvent);
407
this.list.setAnchor(undefined);
408
this.view.domNode.focus();
409
}
410
411
private onEscape(e: StandardKeyboardEvent): void {
412
if (this.list.getSelection().length) {
413
e.preventDefault();
414
e.stopPropagation();
415
this.list.setSelection([], e.browserEvent);
416
this.list.setAnchor(undefined);
417
this.view.domNode.focus();
418
}
419
}
420
421
dispose() {
422
this.disposables.dispose();
423
this.multipleSelectionDisposables.dispose();
424
}
425
}
426
427
export enum TypeNavigationMode {
428
Automatic,
429
Trigger
430
}
431
432
enum TypeNavigationControllerState {
433
Idle,
434
Typing
435
}
436
437
export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardNavigationDelegate {
438
mightProducePrintableCharacter(event: IKeyboardEvent): boolean {
439
if (event.ctrlKey || event.metaKey || event.altKey) {
440
return false;
441
}
442
443
return (event.keyCode >= KeyCode.KeyA && event.keyCode <= KeyCode.KeyZ)
444
|| (event.keyCode >= KeyCode.Digit0 && event.keyCode <= KeyCode.Digit9)
445
|| (event.keyCode >= KeyCode.Numpad0 && event.keyCode <= KeyCode.Numpad9)
446
|| (event.keyCode >= KeyCode.Semicolon && event.keyCode <= KeyCode.Quote);
447
}
448
};
449
450
class TypeNavigationController<T> implements IDisposable {
451
452
private enabled = false;
453
private state: TypeNavigationControllerState = TypeNavigationControllerState.Idle;
454
455
private mode = TypeNavigationMode.Automatic;
456
private triggered = false;
457
private previouslyFocused = -1;
458
459
private readonly enabledDisposables = new DisposableStore();
460
private readonly disposables = new DisposableStore();
461
462
constructor(
463
private list: List<T>,
464
private view: IListView<T>,
465
private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>,
466
private keyboardNavigationEventFilter: IKeyboardNavigationEventFilter,
467
private delegate: IKeyboardNavigationDelegate
468
) {
469
this.updateOptions(list.options);
470
}
471
472
updateOptions(options: IListOptions<T>): void {
473
if (options.typeNavigationEnabled ?? true) {
474
this.enable();
475
} else {
476
this.disable();
477
}
478
479
this.mode = options.typeNavigationMode ?? TypeNavigationMode.Automatic;
480
}
481
482
trigger(): void {
483
this.triggered = !this.triggered;
484
}
485
486
private enable(): void {
487
if (this.enabled) {
488
return;
489
}
490
491
let typing = false;
492
493
const onChar = Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>
494
$.filter(e => !isEditableElement(e.target as HTMLElement))
495
.filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered)
496
.map(event => new StandardKeyboardEvent(event))
497
.filter(e => typing || this.keyboardNavigationEventFilter(e))
498
.filter(e => this.delegate.mightProducePrintableCharacter(e))
499
.forEach(e => EventHelper.stop(e, true))
500
.map(event => event.browserEvent.key)
501
);
502
503
const onClear = Event.debounce<string, null>(onChar, () => null, 800, undefined, undefined, undefined, this.enabledDisposables);
504
const onInput = Event.reduce<string | null, string | null>(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i), undefined, this.enabledDisposables);
505
506
onInput(this.onInput, this, this.enabledDisposables);
507
onClear(this.onClear, this, this.enabledDisposables);
508
509
onChar(() => typing = true, undefined, this.enabledDisposables);
510
onClear(() => typing = false, undefined, this.enabledDisposables);
511
512
this.enabled = true;
513
this.triggered = false;
514
}
515
516
private disable(): void {
517
if (!this.enabled) {
518
return;
519
}
520
521
this.enabledDisposables.clear();
522
this.enabled = false;
523
this.triggered = false;
524
}
525
526
private onClear(): void {
527
const focus = this.list.getFocus();
528
if (focus.length > 0 && focus[0] === this.previouslyFocused) {
529
// List: re-announce element on typing end since typed keys will interrupt aria label of focused element
530
// Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961
531
const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0]));
532
533
if (typeof ariaLabel === 'string') {
534
alert(ariaLabel);
535
} else if (ariaLabel) {
536
alert(ariaLabel.get());
537
}
538
}
539
this.previouslyFocused = -1;
540
}
541
542
private onInput(word: string | null): void {
543
if (!word) {
544
this.state = TypeNavigationControllerState.Idle;
545
this.triggered = false;
546
return;
547
}
548
549
const focus = this.list.getFocus();
550
const start = focus.length > 0 ? focus[0] : 0;
551
const delta = this.state === TypeNavigationControllerState.Idle ? 1 : 0;
552
this.state = TypeNavigationControllerState.Typing;
553
554
for (let i = 0; i < this.list.length; i++) {
555
const index = (start + i + delta) % this.list.length;
556
const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(this.view.element(index));
557
const labelStr = label && label.toString();
558
559
if (this.list.options.typeNavigationEnabled) {
560
if (typeof labelStr !== 'undefined') {
561
562
// If prefix is found, focus and return early
563
if (matchesPrefix(word, labelStr)) {
564
this.previouslyFocused = start;
565
this.list.setFocus([index]);
566
this.list.reveal(index);
567
return;
568
}
569
570
const fuzzy = matchesFuzzy2(word, labelStr);
571
572
if (fuzzy) {
573
const fuzzyScore = fuzzy[0].end - fuzzy[0].start;
574
// ensures that when fuzzy matching, doesn't clash with prefix matching (1 input vs 1+ should be prefix and fuzzy respecitvely). Also makes sure that exact matches are prioritized.
575
if (fuzzyScore > 1 && fuzzy.length === 1) {
576
this.previouslyFocused = start;
577
this.list.setFocus([index]);
578
this.list.reveal(index);
579
return;
580
}
581
}
582
}
583
} else if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) {
584
this.previouslyFocused = start;
585
this.list.setFocus([index]);
586
this.list.reveal(index);
587
return;
588
}
589
}
590
}
591
592
dispose() {
593
this.disable();
594
this.enabledDisposables.dispose();
595
this.disposables.dispose();
596
}
597
}
598
599
class DOMFocusController<T> implements IDisposable {
600
601
private readonly disposables = new DisposableStore();
602
603
constructor(
604
private list: List<T>,
605
private view: IListView<T>
606
) {
607
const onKeyDown = Event.chain(this.disposables.add(new DomEmitter(view.domNode, 'keydown')).event, $ => $
608
.filter(e => !isEditableElement(e.target as HTMLElement))
609
.map(e => new StandardKeyboardEvent(e))
610
);
611
612
const onTab = Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey));
613
614
onTab(this.onTab, this, this.disposables);
615
}
616
617
private onTab(e: StandardKeyboardEvent): void {
618
if (e.target !== this.view.domNode) {
619
return;
620
}
621
622
const focus = this.list.getFocus();
623
624
if (focus.length === 0) {
625
return;
626
}
627
628
const focusedDomElement = this.view.domElement(focus[0]);
629
630
if (!focusedDomElement) {
631
return;
632
}
633
634
const tabIndexElement = focusedDomElement.querySelector('[tabIndex]');
635
636
if (!tabIndexElement || !(isHTMLElement(tabIndexElement)) || tabIndexElement.tabIndex === -1) {
637
return;
638
}
639
640
const style = getWindow(tabIndexElement).getComputedStyle(tabIndexElement);
641
if (style.visibility === 'hidden' || style.display === 'none') {
642
return;
643
}
644
645
e.preventDefault();
646
e.stopPropagation();
647
tabIndexElement.focus();
648
}
649
650
dispose() {
651
this.disposables.dispose();
652
}
653
}
654
655
export function isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
656
return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;
657
}
658
659
export function isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
660
return event.browserEvent.shiftKey;
661
}
662
663
function isMouseRightClick(event: UIEvent): boolean {
664
return isMouseEvent(event) && event.button === 2;
665
}
666
667
const DefaultMultipleSelectionController = {
668
isSelectionSingleChangeEvent,
669
isSelectionRangeChangeEvent
670
};
671
672
export class MouseController<T> implements IDisposable {
673
674
private multipleSelectionController: IMultipleSelectionController<T> | undefined;
675
private readonly mouseSupport: boolean;
676
private readonly disposables = new DisposableStore();
677
678
private readonly _onPointer = this.disposables.add(new Emitter<IListMouseEvent<T>>());
679
get onPointer() { return this._onPointer.event; }
680
681
constructor(protected list: List<T>) {
682
if (list.options.multipleSelectionSupport !== false) {
683
this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController;
684
}
685
686
this.mouseSupport = typeof list.options.mouseSupport === 'undefined' || !!list.options.mouseSupport;
687
688
if (this.mouseSupport) {
689
list.onMouseDown(this.onMouseDown, this, this.disposables);
690
list.onContextMenu(this.onContextMenu, this, this.disposables);
691
list.onMouseDblClick(this.onDoubleClick, this, this.disposables);
692
list.onTouchStart(this.onMouseDown, this, this.disposables);
693
this.disposables.add(Gesture.addTarget(list.getHTMLElement()));
694
}
695
696
Event.any<IListMouseEvent<any> | IListGestureEvent<any>>(list.onMouseClick, list.onMouseMiddleClick, list.onTap)(this.onViewPointer, this, this.disposables);
697
}
698
699
updateOptions(optionsUpdate: IListOptionsUpdate): void {
700
if (optionsUpdate.multipleSelectionSupport !== undefined) {
701
this.multipleSelectionController = undefined;
702
703
if (optionsUpdate.multipleSelectionSupport) {
704
this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController;
705
}
706
}
707
}
708
709
protected isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
710
if (!this.multipleSelectionController) {
711
return false;
712
}
713
714
return this.multipleSelectionController.isSelectionSingleChangeEvent(event);
715
}
716
717
protected isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
718
if (!this.multipleSelectionController) {
719
return false;
720
}
721
722
return this.multipleSelectionController.isSelectionRangeChangeEvent(event);
723
}
724
725
private isSelectionChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
726
return this.isSelectionSingleChangeEvent(event) || this.isSelectionRangeChangeEvent(event);
727
}
728
729
protected onMouseDown(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
730
if (isMonacoEditor(e.browserEvent.target as HTMLElement)) {
731
return;
732
}
733
734
if (getActiveElement() !== e.browserEvent.target) {
735
this.list.domFocus();
736
}
737
}
738
739
protected onContextMenu(e: IListContextMenuEvent<T>): void {
740
if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
741
return;
742
}
743
744
const focus = typeof e.index === 'undefined' ? [] : [e.index];
745
this.list.setFocus(focus, e.browserEvent);
746
}
747
748
protected onViewPointer(e: IListMouseEvent<T>): void {
749
if (!this.mouseSupport) {
750
return;
751
}
752
753
if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
754
return;
755
}
756
757
if (e.browserEvent.isHandledByList) {
758
return;
759
}
760
761
e.browserEvent.isHandledByList = true;
762
const focus = e.index;
763
764
if (typeof focus === 'undefined') {
765
this.list.setFocus([], e.browserEvent);
766
this.list.setSelection([], e.browserEvent);
767
this.list.setAnchor(undefined);
768
return;
769
}
770
771
if (this.isSelectionChangeEvent(e)) {
772
return this.changeSelection(e);
773
}
774
775
this.list.setFocus([focus], e.browserEvent);
776
this.list.setAnchor(focus);
777
778
if (!isMouseRightClick(e.browserEvent)) {
779
this.list.setSelection([focus], e.browserEvent);
780
}
781
782
this._onPointer.fire(e);
783
}
784
785
protected onDoubleClick(e: IListMouseEvent<T>): void {
786
if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
787
return;
788
}
789
790
if (this.isSelectionChangeEvent(e)) {
791
return;
792
}
793
794
if (e.browserEvent.isHandledByList) {
795
return;
796
}
797
798
e.browserEvent.isHandledByList = true;
799
const focus = this.list.getFocus();
800
this.list.setSelection(focus, e.browserEvent);
801
}
802
803
private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
804
const focus = e.index!;
805
let anchor = this.list.getAnchor();
806
807
if (this.isSelectionRangeChangeEvent(e)) {
808
if (typeof anchor === 'undefined') {
809
const currentFocus = this.list.getFocus()[0];
810
anchor = currentFocus ?? focus;
811
this.list.setAnchor(anchor);
812
}
813
814
const min = Math.min(anchor, focus);
815
const max = Math.max(anchor, focus);
816
const rangeSelection = range(min, max + 1);
817
const selection = this.list.getSelection();
818
const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor);
819
820
if (contiguousRange.length === 0) {
821
return;
822
}
823
824
const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange));
825
this.list.setSelection(newSelection, e.browserEvent);
826
this.list.setFocus([focus], e.browserEvent);
827
828
} else if (this.isSelectionSingleChangeEvent(e)) {
829
const selection = this.list.getSelection();
830
const newSelection = selection.filter(i => i !== focus);
831
832
this.list.setFocus([focus]);
833
this.list.setAnchor(focus);
834
835
if (selection.length === newSelection.length) {
836
this.list.setSelection([...newSelection, focus], e.browserEvent);
837
} else {
838
this.list.setSelection(newSelection, e.browserEvent);
839
}
840
}
841
}
842
843
dispose() {
844
this.disposables.dispose();
845
}
846
}
847
848
export interface IMultipleSelectionController<T> {
849
isSelectionSingleChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;
850
isSelectionRangeChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;
851
}
852
853
export interface IStyleController {
854
style(styles: IListStyles): void;
855
}
856
857
export interface IListAccessibilityProvider<T> extends IListViewAccessibilityProvider<T> {
858
getAriaLabel(element: T): string | IObservable<string> | null;
859
getWidgetAriaLabel(): string | IObservable<string>;
860
getWidgetRole?(): AriaRole;
861
getAriaLevel?(element: T): number | undefined;
862
onDidChangeActiveDescendant?: Event<void>;
863
getActiveDescendantId?(element: T): string | undefined;
864
}
865
866
export class DefaultStyleController implements IStyleController {
867
868
constructor(private styleElement: HTMLStyleElement, private selectorSuffix: string) { }
869
870
style(styles: IListStyles): void {
871
const suffix = this.selectorSuffix && `.${this.selectorSuffix}`;
872
const content: string[] = [];
873
874
if (styles.listBackground) {
875
content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`);
876
}
877
878
if (styles.listFocusBackground) {
879
content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`);
880
content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case!
881
}
882
883
if (styles.listFocusForeground) {
884
content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`);
885
}
886
887
if (styles.listActiveSelectionBackground) {
888
content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`);
889
content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case!
890
}
891
892
if (styles.listActiveSelectionForeground) {
893
content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`);
894
}
895
896
if (styles.listActiveSelectionIconForeground) {
897
content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; }`);
898
}
899
900
if (styles.listFocusAndSelectionBackground) {
901
content.push(`
902
.monaco-drag-image${suffix},
903
.monaco-list${suffix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }
904
`);
905
}
906
907
if (styles.listFocusAndSelectionForeground) {
908
content.push(`
909
.monaco-drag-image${suffix},
910
.monaco-list${suffix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }
911
`);
912
}
913
914
if (styles.listInactiveFocusForeground) {
915
content.push(`.monaco-list${suffix} .monaco-list-row.focused { color: ${styles.listInactiveFocusForeground}; }`);
916
content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { color: ${styles.listInactiveFocusForeground}; }`); // overwrite :hover style in this case!
917
}
918
919
if (styles.listInactiveSelectionIconForeground) {
920
content.push(`.monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; }`);
921
}
922
923
if (styles.listInactiveFocusBackground) {
924
content.push(`.monaco-list${suffix} .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`);
925
content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!
926
}
927
928
if (styles.listInactiveSelectionBackground) {
929
content.push(`.monaco-list${suffix} .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`);
930
content.push(`.monaco-list${suffix} .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case!
931
}
932
933
if (styles.listInactiveSelectionForeground) {
934
content.push(`.monaco-list${suffix} .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`);
935
}
936
937
if (styles.listHoverBackground) {
938
content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);
939
}
940
941
if (styles.listHoverForeground) {
942
content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`);
943
}
944
945
/**
946
* Outlines
947
*/
948
const focusAndSelectionOutline = asCssValueWithDefault(styles.listFocusAndSelectionOutline, asCssValueWithDefault(styles.listSelectionOutline, styles.listFocusOutline ?? ''));
949
if (focusAndSelectionOutline) { // default: listFocusOutline
950
content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused.selected { outline: 1px solid ${focusAndSelectionOutline}; outline-offset: -1px;}`);
951
}
952
953
if (styles.listFocusOutline) { // default: set
954
content.push(`
955
.monaco-drag-image${suffix},
956
.monaco-list${suffix}:focus .monaco-list-row.focused,
957
.monaco-workbench.context-menu-visible .monaco-list${suffix}.last-focused .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }
958
`);
959
}
960
961
const inactiveFocusAndSelectionOutline = asCssValueWithDefault(styles.listSelectionOutline, styles.listInactiveFocusOutline ?? '');
962
if (inactiveFocusAndSelectionOutline) {
963
content.push(`.monaco-list${suffix} .monaco-list-row.focused.selected { outline: 1px dotted ${inactiveFocusAndSelectionOutline}; outline-offset: -1px; }`);
964
}
965
966
if (styles.listSelectionOutline) { // default: activeContrastBorder
967
content.push(`.monaco-list${suffix} .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`);
968
}
969
970
if (styles.listInactiveFocusOutline) { // default: null
971
content.push(`.monaco-list${suffix} .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`);
972
}
973
974
if (styles.listHoverOutline) { // default: activeContrastBorder
975
content.push(`.monaco-list${suffix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);
976
}
977
978
if (styles.listDropOverBackground) {
979
content.push(`
980
.monaco-list${suffix}.drop-target,
981
.monaco-list${suffix} .monaco-list-rows.drop-target,
982
.monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropOverBackground} !important; color: inherit !important; }
983
`);
984
}
985
986
if (styles.listDropBetweenBackground) {
987
content.push(`
988
.monaco-list${suffix} .monaco-list-rows.drop-target-before .monaco-list-row:first-child::before,
989
.monaco-list${suffix} .monaco-list-row.drop-target-before::before {
990
content: ""; position: absolute; top: 0px; left: 0px; width: 100%; height: 1px;
991
background-color: ${styles.listDropBetweenBackground};
992
}`);
993
content.push(`
994
.monaco-list${suffix} .monaco-list-rows.drop-target-after .monaco-list-row:last-child::after,
995
.monaco-list${suffix} .monaco-list-row.drop-target-after::after {
996
content: ""; position: absolute; bottom: 0px; left: 0px; width: 100%; height: 1px;
997
background-color: ${styles.listDropBetweenBackground};
998
}`);
999
}
1000
1001
if (styles.tableColumnsBorder) {
1002
content.push(`
1003
.monaco-table > .monaco-split-view2,
1004
.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before,
1005
.monaco-workbench:not(.reduce-motion) .monaco-table:hover > .monaco-split-view2,
1006
.monaco-workbench:not(.reduce-motion) .monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before {
1007
border-color: ${styles.tableColumnsBorder};
1008
}
1009
1010
.monaco-workbench:not(.reduce-motion) .monaco-table > .monaco-split-view2,
1011
.monaco-workbench:not(.reduce-motion) .monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {
1012
border-color: transparent;
1013
}
1014
`);
1015
}
1016
1017
if (styles.tableOddRowsBackgroundColor) {
1018
content.push(`
1019
.monaco-table .monaco-list-row[data-parity=odd]:not(.focused):not(.selected):not(:hover) .monaco-table-tr,
1020
.monaco-table .monaco-list:not(:focus) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr,
1021
.monaco-table .monaco-list:not(.focused) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr {
1022
background-color: ${styles.tableOddRowsBackgroundColor};
1023
}
1024
`);
1025
}
1026
1027
this.styleElement.textContent = content.join('\n');
1028
}
1029
}
1030
1031
export interface IKeyboardNavigationEventFilter {
1032
(e: StandardKeyboardEvent): boolean;
1033
}
1034
1035
export interface IListOptionsUpdate extends IListViewOptionsUpdate {
1036
readonly typeNavigationEnabled?: boolean;
1037
readonly typeNavigationMode?: TypeNavigationMode;
1038
readonly multipleSelectionSupport?: boolean;
1039
}
1040
1041
export interface IListOptions<T> extends IListOptionsUpdate {
1042
readonly identityProvider?: IIdentityProvider<T>;
1043
readonly dnd?: IListDragAndDrop<T>;
1044
readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider<T>;
1045
readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate;
1046
readonly keyboardSupport?: boolean;
1047
readonly multipleSelectionController?: IMultipleSelectionController<T>;
1048
readonly styleController?: (suffix: string) => IStyleController;
1049
readonly accessibilityProvider?: IListAccessibilityProvider<T>;
1050
readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
1051
1052
// list view options
1053
readonly useShadows?: boolean;
1054
readonly verticalScrollMode?: ScrollbarVisibility;
1055
readonly setRowLineHeight?: boolean;
1056
readonly setRowHeight?: boolean;
1057
readonly supportDynamicHeights?: boolean;
1058
readonly mouseSupport?: boolean;
1059
readonly userSelection?: boolean;
1060
readonly horizontalScrolling?: boolean;
1061
readonly scrollByPage?: boolean;
1062
readonly transformOptimization?: boolean;
1063
readonly smoothScrolling?: boolean;
1064
readonly scrollableElementChangeOptions?: ScrollableElementChangeOptions;
1065
readonly alwaysConsumeMouseWheel?: boolean;
1066
readonly initialSize?: Dimension;
1067
readonly paddingTop?: number;
1068
readonly paddingBottom?: number;
1069
}
1070
1071
export interface IListStyles {
1072
listBackground: string | undefined;
1073
listFocusBackground: string | undefined;
1074
listFocusForeground: string | undefined;
1075
listActiveSelectionBackground: string | undefined;
1076
listActiveSelectionForeground: string | undefined;
1077
listActiveSelectionIconForeground: string | undefined;
1078
listFocusAndSelectionOutline: string | undefined;
1079
listFocusAndSelectionBackground: string | undefined;
1080
listFocusAndSelectionForeground: string | undefined;
1081
listInactiveSelectionBackground: string | undefined;
1082
listInactiveSelectionIconForeground: string | undefined;
1083
listInactiveSelectionForeground: string | undefined;
1084
listInactiveFocusForeground: string | undefined;
1085
listInactiveFocusBackground: string | undefined;
1086
listHoverBackground: string | undefined;
1087
listHoverForeground: string | undefined;
1088
listDropOverBackground: string | undefined;
1089
listDropBetweenBackground: string | undefined;
1090
listFocusOutline: string | undefined;
1091
listInactiveFocusOutline: string | undefined;
1092
listSelectionOutline: string | undefined;
1093
listHoverOutline: string | undefined;
1094
treeIndentGuidesStroke: string | undefined;
1095
treeInactiveIndentGuidesStroke: string | undefined;
1096
treeStickyScrollBackground: string | undefined;
1097
treeStickyScrollBorder: string | undefined;
1098
treeStickyScrollShadow: string | undefined;
1099
tableColumnsBorder: string | undefined;
1100
tableOddRowsBackgroundColor: string | undefined;
1101
}
1102
1103
export const unthemedListStyles: IListStyles = {
1104
listFocusBackground: '#7FB0D0',
1105
listActiveSelectionBackground: '#0E639C',
1106
listActiveSelectionForeground: '#FFFFFF',
1107
listActiveSelectionIconForeground: '#FFFFFF',
1108
listFocusAndSelectionOutline: '#90C2F9',
1109
listFocusAndSelectionBackground: '#094771',
1110
listFocusAndSelectionForeground: '#FFFFFF',
1111
listInactiveSelectionBackground: '#3F3F46',
1112
listInactiveSelectionIconForeground: '#FFFFFF',
1113
listHoverBackground: '#2A2D2E',
1114
listDropOverBackground: '#383B3D',
1115
listDropBetweenBackground: '#EEEEEE',
1116
treeIndentGuidesStroke: '#a9a9a9',
1117
treeInactiveIndentGuidesStroke: Color.fromHex('#a9a9a9').transparent(0.4).toString(),
1118
tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2).toString(),
1119
tableOddRowsBackgroundColor: Color.fromHex('#cccccc').transparent(0.04).toString(),
1120
listBackground: undefined,
1121
listFocusForeground: undefined,
1122
listInactiveSelectionForeground: undefined,
1123
listInactiveFocusForeground: undefined,
1124
listInactiveFocusBackground: undefined,
1125
listHoverForeground: undefined,
1126
listFocusOutline: undefined,
1127
listInactiveFocusOutline: undefined,
1128
listSelectionOutline: undefined,
1129
listHoverOutline: undefined,
1130
treeStickyScrollBackground: undefined,
1131
treeStickyScrollBorder: undefined,
1132
treeStickyScrollShadow: undefined
1133
};
1134
1135
const DefaultOptions: IListOptions<any> = {
1136
keyboardSupport: true,
1137
mouseSupport: true,
1138
multipleSelectionSupport: true,
1139
dnd: {
1140
getDragURI() { return null; },
1141
onDragStart(): void { },
1142
onDragOver() { return false; },
1143
drop() { },
1144
dispose() { }
1145
}
1146
};
1147
1148
// TODO@Joao: move these utils into a SortedArray class
1149
1150
function getContiguousRangeContaining(range: number[], value: number): number[] {
1151
const index = range.indexOf(value);
1152
1153
if (index === -1) {
1154
return [];
1155
}
1156
1157
const result: number[] = [];
1158
let i = index - 1;
1159
while (i >= 0 && range[i] === value - (index - i)) {
1160
result.push(range[i--]);
1161
}
1162
1163
result.reverse();
1164
i = index;
1165
while (i < range.length && range[i] === value + (i - index)) {
1166
result.push(range[i++]);
1167
}
1168
1169
return result;
1170
}
1171
1172
/**
1173
* Given two sorted collections of numbers, returns the intersection
1174
* between them (OR).
1175
*/
1176
function disjunction(one: number[], other: number[]): number[] {
1177
const result: number[] = [];
1178
let i = 0, j = 0;
1179
1180
while (i < one.length || j < other.length) {
1181
if (i >= one.length) {
1182
result.push(other[j++]);
1183
} else if (j >= other.length) {
1184
result.push(one[i++]);
1185
} else if (one[i] === other[j]) {
1186
result.push(one[i]);
1187
i++;
1188
j++;
1189
continue;
1190
} else if (one[i] < other[j]) {
1191
result.push(one[i++]);
1192
} else {
1193
result.push(other[j++]);
1194
}
1195
}
1196
1197
return result;
1198
}
1199
1200
/**
1201
* Given two sorted collections of numbers, returns the relative
1202
* complement between them (XOR).
1203
*/
1204
function relativeComplement(one: number[], other: number[]): number[] {
1205
const result: number[] = [];
1206
let i = 0, j = 0;
1207
1208
while (i < one.length || j < other.length) {
1209
if (i >= one.length) {
1210
result.push(other[j++]);
1211
} else if (j >= other.length) {
1212
result.push(one[i++]);
1213
} else if (one[i] === other[j]) {
1214
i++;
1215
j++;
1216
continue;
1217
} else if (one[i] < other[j]) {
1218
result.push(one[i++]);
1219
} else {
1220
j++;
1221
}
1222
}
1223
1224
return result;
1225
}
1226
1227
const numericSort = (a: number, b: number) => a - b;
1228
1229
class PipelineRenderer<T> implements IListRenderer<T, any> {
1230
1231
constructor(
1232
private _templateId: string,
1233
private renderers: IListRenderer<any /* TODO@joao */, any>[]
1234
) { }
1235
1236
get templateId(): string {
1237
return this._templateId;
1238
}
1239
1240
renderTemplate(container: HTMLElement): any[] {
1241
return this.renderers.map(r => r.renderTemplate(container));
1242
}
1243
1244
renderElement(element: T, index: number, templateData: any[], renderDetails?: IListElementRenderDetails): void {
1245
let i = 0;
1246
1247
for (const renderer of this.renderers) {
1248
renderer.renderElement(element, index, templateData[i++], renderDetails);
1249
}
1250
}
1251
1252
disposeElement(element: T, index: number, templateData: any[], renderDetails?: IListElementRenderDetails): void {
1253
let i = 0;
1254
1255
for (const renderer of this.renderers) {
1256
renderer.disposeElement?.(element, index, templateData[i], renderDetails);
1257
1258
i += 1;
1259
}
1260
}
1261
1262
disposeTemplate(templateData: unknown[]): void {
1263
let i = 0;
1264
1265
for (const renderer of this.renderers) {
1266
renderer.disposeTemplate(templateData[i++]);
1267
}
1268
}
1269
}
1270
1271
class AccessibiltyRenderer<T> implements IListRenderer<T, IAccessibilityTemplateData> {
1272
1273
templateId: string = 'a18n';
1274
1275
constructor(private accessibilityProvider: IListAccessibilityProvider<T>) { }
1276
1277
renderTemplate(container: HTMLElement): IAccessibilityTemplateData {
1278
return { container, disposables: new DisposableStore() };
1279
}
1280
1281
renderElement(element: T, index: number, data: IAccessibilityTemplateData): void {
1282
const ariaLabel = this.accessibilityProvider.getAriaLabel(element);
1283
const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel);
1284
1285
data.disposables.add(autorun(reader => {
1286
this.setAriaLabel(reader.readObservable(observable), data.container);
1287
}));
1288
1289
const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element);
1290
1291
if (typeof ariaLevel === 'number') {
1292
data.container.setAttribute('aria-level', `${ariaLevel}`);
1293
} else {
1294
data.container.removeAttribute('aria-level');
1295
}
1296
}
1297
1298
private setAriaLabel(ariaLabel: string | null, element: HTMLElement): void {
1299
if (ariaLabel) {
1300
element.setAttribute('aria-label', ariaLabel);
1301
} else {
1302
element.removeAttribute('aria-label');
1303
}
1304
}
1305
1306
disposeElement(element: T, index: number, templateData: IAccessibilityTemplateData): void {
1307
templateData.disposables.clear();
1308
}
1309
1310
disposeTemplate(templateData: IAccessibilityTemplateData): void {
1311
templateData.disposables.dispose();
1312
}
1313
}
1314
1315
class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
1316
1317
constructor(private list: List<T>, private dnd: IListDragAndDrop<T>) { }
1318
1319
getDragElements(element: T): T[] {
1320
const selection = this.list.getSelectedElements();
1321
const elements = selection.indexOf(element) > -1 ? selection : [element];
1322
return elements;
1323
}
1324
1325
getDragURI(element: T): string | null {
1326
return this.dnd.getDragURI(element);
1327
}
1328
1329
getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined {
1330
if (this.dnd.getDragLabel) {
1331
return this.dnd.getDragLabel(elements, originalEvent);
1332
}
1333
1334
return undefined;
1335
}
1336
1337
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
1338
this.dnd.onDragStart?.(data, originalEvent);
1339
}
1340
1341
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction {
1342
return this.dnd.onDragOver(data, targetElement, targetIndex, targetSector, originalEvent);
1343
}
1344
1345
onDragLeave(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
1346
this.dnd.onDragLeave?.(data, targetElement, targetIndex, originalEvent);
1347
}
1348
1349
onDragEnd(originalEvent: DragEvent): void {
1350
this.dnd.onDragEnd?.(originalEvent);
1351
}
1352
1353
drop(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void {
1354
this.dnd.drop(data, targetElement, targetIndex, targetSector, originalEvent);
1355
}
1356
1357
dispose(): void {
1358
this.dnd.dispose();
1359
}
1360
}
1361
1362
/**
1363
* The {@link List} is a virtual scrolling widget, built on top of the {@link ListView}
1364
* widget.
1365
*
1366
* Features:
1367
* - Customizable keyboard and mouse support
1368
* - Element traits: focus, selection, achor
1369
* - Accessibility support
1370
* - Touch support
1371
* - Performant template-based rendering
1372
* - Horizontal scrolling
1373
* - Variable element height support
1374
* - Dynamic element height support
1375
* - Drag-and-drop support
1376
*/
1377
export class List<T> implements ISpliceable<T>, IDisposable {
1378
1379
private focus = new Trait<T>('focused');
1380
private selection: Trait<T>;
1381
private anchor = new Trait<T>('anchor');
1382
private eventBufferer = new EventBufferer();
1383
protected view: IListView<T>;
1384
private spliceable: ISpliceable<T>;
1385
private styleController: IStyleController;
1386
private typeNavigationController?: TypeNavigationController<T>;
1387
private accessibilityProvider?: IListAccessibilityProvider<T>;
1388
private keyboardController: KeyboardController<T> | undefined;
1389
private mouseController: MouseController<T>;
1390
private _ariaLabel: string = '';
1391
1392
protected readonly disposables = new DisposableStore();
1393
1394
@memoize get onDidChangeFocus(): Event<IListEvent<T>> {
1395
return Event.map(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toListEvent(e), this.disposables);
1396
}
1397
1398
@memoize get onDidChangeSelection(): Event<IListEvent<T>> {
1399
return Event.map(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toListEvent(e), this.disposables);
1400
}
1401
1402
get domId(): string { return this.view.domId; }
1403
get onDidScroll(): Event<ScrollEvent> { return this.view.onDidScroll; }
1404
get onMouseClick(): Event<IListMouseEvent<T>> { return this.view.onMouseClick; }
1405
get onMouseDblClick(): Event<IListMouseEvent<T>> { return this.view.onMouseDblClick; }
1406
get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return this.view.onMouseMiddleClick; }
1407
get onPointer(): Event<IListMouseEvent<T>> { return this.mouseController.onPointer; }
1408
get onMouseUp(): Event<IListMouseEvent<T>> { return this.view.onMouseUp; }
1409
get onMouseDown(): Event<IListMouseEvent<T>> { return this.view.onMouseDown; }
1410
get onMouseOver(): Event<IListMouseEvent<T>> { return this.view.onMouseOver; }
1411
get onMouseMove(): Event<IListMouseEvent<T>> { return this.view.onMouseMove; }
1412
get onMouseOut(): Event<IListMouseEvent<T>> { return this.view.onMouseOut; }
1413
get onTouchStart(): Event<IListTouchEvent<T>> { return this.view.onTouchStart; }
1414
get onTap(): Event<IListGestureEvent<T>> { return this.view.onTap; }
1415
1416
/**
1417
* Possible context menu trigger events:
1418
* - ContextMenu key
1419
* - Shift F10
1420
* - Ctrl Option Shift M (macOS with VoiceOver)
1421
* - Mouse right click
1422
*/
1423
@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
1424
let didJustPressContextMenuKey = false;
1425
1426
const fromKeyDown: Event<any> = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>
1427
$.map(e => new StandardKeyboardEvent(e))
1428
.filter(e => didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
1429
.map(e => EventHelper.stop(e, true))
1430
.filter(() => false));
1431
1432
const fromKeyUp = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event, $ =>
1433
$.forEach(() => didJustPressContextMenuKey = false)
1434
.map(e => new StandardKeyboardEvent(e))
1435
.filter(e => e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
1436
.map(e => EventHelper.stop(e, true))
1437
.map(({ browserEvent }) => {
1438
const focus = this.getFocus();
1439
const index = focus.length ? focus[0] : undefined;
1440
const element = typeof index !== 'undefined' ? this.view.element(index) : undefined;
1441
const anchor = typeof index !== 'undefined' ? this.view.domElement(index) as HTMLElement : this.view.domNode;
1442
return { index, element, anchor, browserEvent };
1443
}));
1444
1445
const fromMouse = Event.chain(this.view.onContextMenu, $ =>
1446
$.filter(_ => !didJustPressContextMenuKey)
1447
.map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(getWindow(this.view.domNode), browserEvent), browserEvent }))
1448
);
1449
1450
return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);
1451
}
1452
1453
@memoize get onKeyDown(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event; }
1454
@memoize get onKeyUp(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event; }
1455
@memoize get onKeyPress(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keypress')).event; }
1456
1457
@memoize get onDidFocus(): Event<void> { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'focus', true)).event); }
1458
@memoize get onDidBlur(): Event<void> { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'blur', true)).event); }
1459
1460
private readonly _onDidDispose = new Emitter<void>();
1461
readonly onDidDispose: Event<void> = this._onDidDispose.event;
1462
1463
constructor(
1464
private user: string,
1465
container: HTMLElement,
1466
virtualDelegate: IListVirtualDelegate<T>,
1467
renderers: IListRenderer<any /* TODO@joao */, any>[],
1468
private _options: IListOptions<T> = DefaultOptions
1469
) {
1470
const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list';
1471
this.selection = new SelectionTrait(role !== 'listbox');
1472
1473
const baseRenderers: IListRenderer<T, unknown>[] = [this.focus.renderer, this.selection.renderer];
1474
1475
this.accessibilityProvider = _options.accessibilityProvider;
1476
1477
if (this.accessibilityProvider) {
1478
baseRenderers.push(new AccessibiltyRenderer<T>(this.accessibilityProvider));
1479
1480
this.accessibilityProvider.onDidChangeActiveDescendant?.(this.onDidChangeActiveDescendant, this, this.disposables);
1481
}
1482
1483
renderers = renderers.map(r => new PipelineRenderer(r.templateId, [...baseRenderers, r]));
1484
1485
const viewOptions: IListViewOptions<T> = {
1486
..._options,
1487
dnd: _options.dnd && new ListViewDragAndDrop(this, _options.dnd)
1488
};
1489
1490
this.view = this.createListView(container, virtualDelegate, renderers, viewOptions);
1491
this.view.domNode.setAttribute('role', role);
1492
1493
if (_options.styleController) {
1494
this.styleController = _options.styleController(this.view.domId);
1495
} else {
1496
const styleElement = createStyleSheet(this.view.domNode);
1497
this.styleController = new DefaultStyleController(styleElement, this.view.domId);
1498
}
1499
1500
this.spliceable = new CombinedSpliceable([
1501
new TraitSpliceable(this.focus, this.view, _options.identityProvider),
1502
new TraitSpliceable(this.selection, this.view, _options.identityProvider),
1503
new TraitSpliceable(this.anchor, this.view, _options.identityProvider),
1504
this.view
1505
]);
1506
1507
this.disposables.add(this.focus);
1508
this.disposables.add(this.selection);
1509
this.disposables.add(this.anchor);
1510
this.disposables.add(this.view);
1511
this.disposables.add(this._onDidDispose);
1512
1513
this.disposables.add(new DOMFocusController(this, this.view));
1514
1515
if (typeof _options.keyboardSupport !== 'boolean' || _options.keyboardSupport) {
1516
this.keyboardController = new KeyboardController(this, this.view, _options);
1517
this.disposables.add(this.keyboardController);
1518
}
1519
1520
if (_options.keyboardNavigationLabelProvider) {
1521
const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;
1522
this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, _options.keyboardNavigationEventFilter ?? (() => true), delegate);
1523
this.disposables.add(this.typeNavigationController);
1524
}
1525
1526
this.mouseController = this.createMouseController(_options);
1527
this.disposables.add(this.mouseController);
1528
1529
this.onDidChangeFocus(this._onFocusChange, this, this.disposables);
1530
this.onDidChangeSelection(this._onSelectionChange, this, this.disposables);
1531
1532
if (this.accessibilityProvider) {
1533
const ariaLabel = this.accessibilityProvider.getWidgetAriaLabel();
1534
const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel);
1535
1536
this.disposables.add(autorun(reader => {
1537
this.ariaLabel = reader.readObservable(observable);
1538
}));
1539
}
1540
1541
if (this._options.multipleSelectionSupport !== false) {
1542
this.view.domNode.setAttribute('aria-multiselectable', 'true');
1543
}
1544
}
1545
1546
protected createListView(container: HTMLElement, virtualDelegate: IListVirtualDelegate<T>, renderers: IListRenderer<any, any>[], viewOptions: IListViewOptions<T>): IListView<T> {
1547
return new ListView(container, virtualDelegate, renderers, viewOptions);
1548
}
1549
1550
protected createMouseController(options: IListOptions<T>): MouseController<T> {
1551
return new MouseController(this);
1552
}
1553
1554
updateOptions(optionsUpdate: IListOptionsUpdate = {}): void {
1555
this._options = { ...this._options, ...optionsUpdate };
1556
1557
this.typeNavigationController?.updateOptions(this._options);
1558
1559
if (this._options.multipleSelectionController !== undefined) {
1560
if (this._options.multipleSelectionSupport) {
1561
this.view.domNode.setAttribute('aria-multiselectable', 'true');
1562
} else {
1563
this.view.domNode.removeAttribute('aria-multiselectable');
1564
}
1565
}
1566
1567
this.mouseController.updateOptions(optionsUpdate);
1568
this.keyboardController?.updateOptions(optionsUpdate);
1569
this.view.updateOptions(optionsUpdate);
1570
}
1571
1572
get options(): IListOptions<T> {
1573
return this._options;
1574
}
1575
1576
splice(start: number, deleteCount: number, elements: readonly T[] = []): void {
1577
if (start < 0 || start > this.view.length) {
1578
throw new ListError(this.user, `Invalid start index: ${start}`);
1579
}
1580
1581
if (deleteCount < 0) {
1582
throw new ListError(this.user, `Invalid delete count: ${deleteCount}`);
1583
}
1584
1585
if (deleteCount === 0 && elements.length === 0) {
1586
return;
1587
}
1588
1589
this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements));
1590
}
1591
1592
updateWidth(index: number): void {
1593
this.view.updateWidth(index);
1594
}
1595
1596
updateElementHeight(index: number, size: number | undefined): void {
1597
this.view.updateElementHeight(index, size, null);
1598
}
1599
1600
rerender(): void {
1601
this.view.rerender();
1602
}
1603
1604
element(index: number): T {
1605
return this.view.element(index);
1606
}
1607
1608
indexOf(element: T): number {
1609
return this.view.indexOf(element);
1610
}
1611
1612
indexAt(position: number): number {
1613
return this.view.indexAt(position);
1614
}
1615
1616
get length(): number {
1617
return this.view.length;
1618
}
1619
1620
get contentHeight(): number {
1621
return this.view.contentHeight;
1622
}
1623
1624
get contentWidth(): number {
1625
return this.view.contentWidth;
1626
}
1627
1628
get onDidChangeContentHeight(): Event<number> {
1629
return this.view.onDidChangeContentHeight;
1630
}
1631
1632
get onDidChangeContentWidth(): Event<number> {
1633
return this.view.onDidChangeContentWidth;
1634
}
1635
1636
get scrollTop(): number {
1637
return this.view.getScrollTop();
1638
}
1639
1640
set scrollTop(scrollTop: number) {
1641
this.view.setScrollTop(scrollTop);
1642
}
1643
1644
get scrollLeft(): number {
1645
return this.view.getScrollLeft();
1646
}
1647
1648
set scrollLeft(scrollLeft: number) {
1649
this.view.setScrollLeft(scrollLeft);
1650
}
1651
1652
get scrollHeight(): number {
1653
return this.view.scrollHeight;
1654
}
1655
1656
get renderHeight(): number {
1657
return this.view.renderHeight;
1658
}
1659
1660
get firstVisibleIndex(): number {
1661
return this.view.firstVisibleIndex;
1662
}
1663
1664
get firstMostlyVisibleIndex(): number {
1665
return this.view.firstMostlyVisibleIndex;
1666
}
1667
1668
get lastVisibleIndex(): number {
1669
return this.view.lastVisibleIndex;
1670
}
1671
1672
get ariaLabel(): string {
1673
return this._ariaLabel;
1674
}
1675
1676
set ariaLabel(value: string) {
1677
this._ariaLabel = value;
1678
this.view.domNode.setAttribute('aria-label', value);
1679
}
1680
1681
domFocus(): void {
1682
this.view.domNode.focus({ preventScroll: true });
1683
}
1684
1685
layout(height?: number, width?: number): void {
1686
this.view.layout(height, width);
1687
}
1688
1689
triggerTypeNavigation(): void {
1690
this.typeNavigationController?.trigger();
1691
}
1692
1693
setSelection(indexes: number[], browserEvent?: UIEvent): void {
1694
for (const index of indexes) {
1695
if (index < 0 || index >= this.length) {
1696
throw new ListError(this.user, `Invalid index ${index}`);
1697
}
1698
}
1699
1700
this.selection.set(indexes, browserEvent);
1701
}
1702
1703
getSelection(): number[] {
1704
return this.selection.get();
1705
}
1706
1707
getSelectedElements(): T[] {
1708
return this.getSelection().map(i => this.view.element(i));
1709
}
1710
1711
setAnchor(index: number | undefined): void {
1712
if (typeof index === 'undefined') {
1713
this.anchor.set([]);
1714
return;
1715
}
1716
1717
if (index < 0 || index >= this.length) {
1718
throw new ListError(this.user, `Invalid index ${index}`);
1719
}
1720
1721
this.anchor.set([index]);
1722
}
1723
1724
getAnchor(): number | undefined {
1725
return this.anchor.get().at(0);
1726
}
1727
1728
getAnchorElement(): T | undefined {
1729
const anchor = this.getAnchor();
1730
return typeof anchor === 'undefined' ? undefined : this.element(anchor);
1731
}
1732
1733
setFocus(indexes: number[], browserEvent?: UIEvent): void {
1734
for (const index of indexes) {
1735
if (index < 0 || index >= this.length) {
1736
throw new ListError(this.user, `Invalid index ${index}`);
1737
}
1738
}
1739
1740
this.focus.set(indexes, browserEvent);
1741
}
1742
1743
focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1744
if (this.length === 0) { return; }
1745
1746
const focus = this.focus.get();
1747
const index = this.findNextIndex(focus.length > 0 ? focus[0] + n : 0, loop, filter);
1748
1749
if (index > -1) {
1750
this.setFocus([index], browserEvent);
1751
}
1752
}
1753
1754
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1755
if (this.length === 0) { return; }
1756
1757
const focus = this.focus.get();
1758
const index = this.findPreviousIndex(focus.length > 0 ? focus[0] - n : 0, loop, filter);
1759
1760
if (index > -1) {
1761
this.setFocus([index], browserEvent);
1762
}
1763
}
1764
1765
async focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): Promise<void> {
1766
let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight);
1767
lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;
1768
const currentlyFocusedElementIndex = this.getFocus()[0];
1769
1770
if (currentlyFocusedElementIndex !== lastPageIndex && (currentlyFocusedElementIndex === undefined || lastPageIndex > currentlyFocusedElementIndex)) {
1771
const lastGoodPageIndex = this.findPreviousIndex(lastPageIndex, false, filter);
1772
1773
if (lastGoodPageIndex > -1 && currentlyFocusedElementIndex !== lastGoodPageIndex) {
1774
this.setFocus([lastGoodPageIndex], browserEvent);
1775
} else {
1776
this.setFocus([lastPageIndex], browserEvent);
1777
}
1778
} else {
1779
const previousScrollTop = this.view.getScrollTop();
1780
let nextpageScrollTop = previousScrollTop + this.view.renderHeight;
1781
if (lastPageIndex > currentlyFocusedElementIndex) {
1782
// scroll last page element to the top only if the last page element is below the focused element
1783
nextpageScrollTop -= this.view.elementHeight(lastPageIndex);
1784
}
1785
1786
this.view.setScrollTop(nextpageScrollTop);
1787
1788
if (this.view.getScrollTop() !== previousScrollTop) {
1789
this.setFocus([]);
1790
1791
// Let the scroll event listener run
1792
await timeout(0);
1793
await this.focusNextPage(browserEvent, filter);
1794
}
1795
}
1796
}
1797
1798
async focusPreviousPage(browserEvent?: UIEvent, filter?: (element: T) => boolean, getPaddingTop: () => number = () => 0): Promise<void> {
1799
let firstPageIndex: number;
1800
const paddingTop = getPaddingTop();
1801
const scrollTop = this.view.getScrollTop() + paddingTop;
1802
1803
if (scrollTop === 0) {
1804
firstPageIndex = this.view.indexAt(scrollTop);
1805
} else {
1806
firstPageIndex = this.view.indexAfter(scrollTop - 1);
1807
}
1808
1809
const currentlyFocusedElementIndex = this.getFocus()[0];
1810
1811
if (currentlyFocusedElementIndex !== firstPageIndex && (currentlyFocusedElementIndex === undefined || currentlyFocusedElementIndex >= firstPageIndex)) {
1812
const firstGoodPageIndex = this.findNextIndex(firstPageIndex, false, filter);
1813
1814
if (firstGoodPageIndex > -1 && currentlyFocusedElementIndex !== firstGoodPageIndex) {
1815
this.setFocus([firstGoodPageIndex], browserEvent);
1816
} else {
1817
this.setFocus([firstPageIndex], browserEvent);
1818
}
1819
} else {
1820
const previousScrollTop = scrollTop;
1821
this.view.setScrollTop(scrollTop - this.view.renderHeight - paddingTop);
1822
1823
if (this.view.getScrollTop() + getPaddingTop() !== previousScrollTop) {
1824
this.setFocus([]);
1825
1826
// Let the scroll event listener run
1827
await timeout(0);
1828
await this.focusPreviousPage(browserEvent, filter, getPaddingTop);
1829
}
1830
}
1831
}
1832
1833
focusLast(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1834
if (this.length === 0) { return; }
1835
1836
const index = this.findPreviousIndex(this.length - 1, false, filter);
1837
1838
if (index > -1) {
1839
this.setFocus([index], browserEvent);
1840
}
1841
}
1842
1843
focusFirst(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1844
this.focusNth(0, browserEvent, filter);
1845
}
1846
1847
focusNth(n: number, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1848
if (this.length === 0) { return; }
1849
1850
const index = this.findNextIndex(n, false, filter);
1851
1852
if (index > -1) {
1853
this.setFocus([index], browserEvent);
1854
}
1855
}
1856
1857
private findNextIndex(index: number, loop = false, filter?: (element: T) => boolean): number {
1858
for (let i = 0; i < this.length; i++) {
1859
if (index >= this.length && !loop) {
1860
return -1;
1861
}
1862
1863
index = index % this.length;
1864
1865
if (!filter || filter(this.element(index))) {
1866
return index;
1867
}
1868
1869
index++;
1870
}
1871
1872
return -1;
1873
}
1874
1875
private findPreviousIndex(index: number, loop = false, filter?: (element: T) => boolean): number {
1876
for (let i = 0; i < this.length; i++) {
1877
if (index < 0 && !loop) {
1878
return -1;
1879
}
1880
1881
index = (this.length + (index % this.length)) % this.length;
1882
1883
if (!filter || filter(this.element(index))) {
1884
return index;
1885
}
1886
1887
index--;
1888
}
1889
1890
return -1;
1891
}
1892
1893
getFocus(): number[] {
1894
return this.focus.get();
1895
}
1896
1897
getFocusedElements(): T[] {
1898
return this.getFocus().map(i => this.view.element(i));
1899
}
1900
1901
reveal(index: number, relativeTop?: number, paddingTop: number = 0): void {
1902
if (index < 0 || index >= this.length) {
1903
throw new ListError(this.user, `Invalid index ${index}`);
1904
}
1905
1906
const scrollTop = this.view.getScrollTop();
1907
const elementTop = this.view.elementTop(index);
1908
const elementHeight = this.view.elementHeight(index);
1909
1910
if (isNumber(relativeTop)) {
1911
// y = mx + b
1912
const m = elementHeight - this.view.renderHeight + paddingTop;
1913
this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop - paddingTop);
1914
} else {
1915
const viewItemBottom = elementTop + elementHeight;
1916
const scrollBottom = scrollTop + this.view.renderHeight;
1917
1918
if (elementTop < scrollTop + paddingTop && viewItemBottom >= scrollBottom) {
1919
// The element is already overflowing the viewport, no-op
1920
} else if (elementTop < scrollTop + paddingTop || (viewItemBottom >= scrollBottom && elementHeight >= this.view.renderHeight)) {
1921
this.view.setScrollTop(elementTop - paddingTop);
1922
} else if (viewItemBottom >= scrollBottom) {
1923
this.view.setScrollTop(viewItemBottom - this.view.renderHeight);
1924
}
1925
}
1926
}
1927
1928
/**
1929
* Returns the relative position of an element rendered in the list.
1930
* Returns `null` if the element isn't *entirely* in the visible viewport.
1931
*/
1932
getRelativeTop(index: number, paddingTop: number = 0): number | null {
1933
if (index < 0 || index >= this.length) {
1934
throw new ListError(this.user, `Invalid index ${index}`);
1935
}
1936
1937
const scrollTop = this.view.getScrollTop();
1938
const elementTop = this.view.elementTop(index);
1939
const elementHeight = this.view.elementHeight(index);
1940
1941
if (elementTop < scrollTop + paddingTop || elementTop + elementHeight > scrollTop + this.view.renderHeight) {
1942
return null;
1943
}
1944
1945
// y = mx + b
1946
const m = elementHeight - this.view.renderHeight + paddingTop;
1947
return Math.abs((scrollTop + paddingTop - elementTop) / m);
1948
}
1949
1950
isDOMFocused(): boolean {
1951
return isActiveElement(this.view.domNode);
1952
}
1953
1954
getHTMLElement(): HTMLElement {
1955
return this.view.domNode;
1956
}
1957
1958
getScrollableElement(): HTMLElement {
1959
return this.view.scrollableElementDomNode;
1960
}
1961
1962
getElementID(index: number): string {
1963
return this.view.getElementDomId(index);
1964
}
1965
1966
getElementTop(index: number): number {
1967
return this.view.elementTop(index);
1968
}
1969
1970
style(styles: IListStyles): void {
1971
this.styleController.style(styles);
1972
}
1973
1974
delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
1975
this.view.delegateScrollFromMouseWheelEvent(browserEvent);
1976
}
1977
1978
private toListEvent({ indexes, browserEvent }: ITraitChangeEvent) {
1979
return { indexes, elements: indexes.map(i => this.view.element(i)), browserEvent };
1980
}
1981
1982
private _onFocusChange(): void {
1983
const focus = this.focus.get();
1984
this.view.domNode.classList.toggle('element-focused', focus.length > 0);
1985
this.onDidChangeActiveDescendant();
1986
}
1987
1988
private onDidChangeActiveDescendant(): void {
1989
const focus = this.focus.get();
1990
1991
if (focus.length > 0) {
1992
let id: string | undefined;
1993
1994
if (this.accessibilityProvider?.getActiveDescendantId) {
1995
id = this.accessibilityProvider.getActiveDescendantId(this.view.element(focus[0]));
1996
}
1997
1998
this.view.domNode.setAttribute('aria-activedescendant', id || this.view.getElementDomId(focus[0]));
1999
} else {
2000
this.view.domNode.removeAttribute('aria-activedescendant');
2001
}
2002
}
2003
2004
private _onSelectionChange(): void {
2005
const selection = this.selection.get();
2006
2007
this.view.domNode.classList.toggle('selection-none', selection.length === 0);
2008
this.view.domNode.classList.toggle('selection-single', selection.length === 1);
2009
this.view.domNode.classList.toggle('selection-multiple', selection.length > 1);
2010
}
2011
2012
dispose(): void {
2013
this._onDidDispose.fire();
2014
this.disposables.dispose();
2015
2016
this._onDidDispose.dispose();
2017
}
2018
}
2019
2020