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
5220 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, NotSelectableGroupId, NotSelectableGroupIdType } 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
407
let selection = range(this.list.length);
408
409
// Filter by group if identity provider has getGroupId
410
const focusedElements = this.list.getFocus();
411
const referenceGroupId = focusedElements.length > 0 ? this.list.getElementGroupId(focusedElements[0]) : undefined;
412
if (referenceGroupId !== undefined) {
413
selection = this.list.filterIndicesByGroup(selection, referenceGroupId);
414
}
415
416
this.list.setSelection(selection, e.browserEvent);
417
this.list.setAnchor(undefined);
418
this.view.domNode.focus();
419
}
420
421
private onEscape(e: StandardKeyboardEvent): void {
422
if (this.list.getSelection().length) {
423
e.preventDefault();
424
e.stopPropagation();
425
this.list.setSelection([], e.browserEvent);
426
this.list.setAnchor(undefined);
427
this.view.domNode.focus();
428
}
429
}
430
431
dispose() {
432
this.disposables.dispose();
433
this.multipleSelectionDisposables.dispose();
434
}
435
}
436
437
export enum TypeNavigationMode {
438
Automatic,
439
Trigger
440
}
441
442
enum TypeNavigationControllerState {
443
Idle,
444
Typing
445
}
446
447
export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardNavigationDelegate {
448
mightProducePrintableCharacter(event: IKeyboardEvent): boolean {
449
if (event.ctrlKey || event.metaKey || event.altKey) {
450
return false;
451
}
452
453
return (event.keyCode >= KeyCode.KeyA && event.keyCode <= KeyCode.KeyZ)
454
|| (event.keyCode >= KeyCode.Digit0 && event.keyCode <= KeyCode.Digit9)
455
|| (event.keyCode >= KeyCode.Numpad0 && event.keyCode <= KeyCode.Numpad9)
456
|| (event.keyCode >= KeyCode.Semicolon && event.keyCode <= KeyCode.Quote);
457
}
458
};
459
460
class TypeNavigationController<T> implements IDisposable {
461
462
private enabled = false;
463
private state: TypeNavigationControllerState = TypeNavigationControllerState.Idle;
464
465
private mode = TypeNavigationMode.Automatic;
466
private triggered = false;
467
private previouslyFocused = -1;
468
469
private readonly enabledDisposables = new DisposableStore();
470
private readonly disposables = new DisposableStore();
471
472
constructor(
473
private list: List<T>,
474
private view: IListView<T>,
475
private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>,
476
private keyboardNavigationEventFilter: IKeyboardNavigationEventFilter,
477
private delegate: IKeyboardNavigationDelegate
478
) {
479
this.updateOptions(list.options);
480
}
481
482
updateOptions(options: IListOptions<T>): void {
483
if (options.typeNavigationEnabled ?? true) {
484
this.enable();
485
} else {
486
this.disable();
487
}
488
489
this.mode = options.typeNavigationMode ?? TypeNavigationMode.Automatic;
490
}
491
492
trigger(): void {
493
this.triggered = !this.triggered;
494
}
495
496
private enable(): void {
497
if (this.enabled) {
498
return;
499
}
500
501
let typing = false;
502
503
const onChar = Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>
504
$.filter(e => !isEditableElement(e.target as HTMLElement))
505
.filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered)
506
.map(event => new StandardKeyboardEvent(event))
507
.filter(e => typing || this.keyboardNavigationEventFilter(e))
508
.filter(e => this.delegate.mightProducePrintableCharacter(e))
509
.forEach(e => EventHelper.stop(e, true))
510
.map(event => event.browserEvent.key)
511
);
512
513
const onClear = Event.debounce<string, null>(onChar, () => null, 800, undefined, undefined, undefined, this.enabledDisposables);
514
const onInput = Event.reduce<string | null, string | null>(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i), undefined, this.enabledDisposables);
515
516
onInput(this.onInput, this, this.enabledDisposables);
517
onClear(this.onClear, this, this.enabledDisposables);
518
519
onChar(() => typing = true, undefined, this.enabledDisposables);
520
onClear(() => typing = false, undefined, this.enabledDisposables);
521
522
this.enabled = true;
523
this.triggered = false;
524
}
525
526
private disable(): void {
527
if (!this.enabled) {
528
return;
529
}
530
531
this.enabledDisposables.clear();
532
this.enabled = false;
533
this.triggered = false;
534
}
535
536
private onClear(): void {
537
const focus = this.list.getFocus();
538
if (focus.length > 0 && focus[0] === this.previouslyFocused) {
539
// List: re-announce element on typing end since typed keys will interrupt aria label of focused element
540
// Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961
541
const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0]));
542
543
if (typeof ariaLabel === 'string') {
544
alert(ariaLabel);
545
} else if (ariaLabel) {
546
alert(ariaLabel.get());
547
}
548
}
549
this.previouslyFocused = -1;
550
}
551
552
private onInput(word: string | null): void {
553
if (!word) {
554
this.state = TypeNavigationControllerState.Idle;
555
this.triggered = false;
556
return;
557
}
558
559
const focus = this.list.getFocus();
560
const start = focus.length > 0 ? focus[0] : 0;
561
const delta = this.state === TypeNavigationControllerState.Idle ? 1 : 0;
562
this.state = TypeNavigationControllerState.Typing;
563
564
for (let i = 0; i < this.list.length; i++) {
565
const index = (start + i + delta) % this.list.length;
566
const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(this.view.element(index));
567
const labelStr = label && label.toString();
568
569
if (this.list.options.typeNavigationEnabled) {
570
if (typeof labelStr !== 'undefined') {
571
572
// If prefix is found, focus and return early
573
if (matchesPrefix(word, labelStr)) {
574
this.previouslyFocused = start;
575
this.list.setFocus([index]);
576
this.list.reveal(index);
577
return;
578
}
579
580
const fuzzy = matchesFuzzy2(word, labelStr);
581
582
if (fuzzy) {
583
const fuzzyScore = fuzzy[0].end - fuzzy[0].start;
584
// 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.
585
if (fuzzyScore > 1 && fuzzy.length === 1) {
586
this.previouslyFocused = start;
587
this.list.setFocus([index]);
588
this.list.reveal(index);
589
return;
590
}
591
}
592
}
593
} else if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) {
594
this.previouslyFocused = start;
595
this.list.setFocus([index]);
596
this.list.reveal(index);
597
return;
598
}
599
}
600
}
601
602
dispose() {
603
this.disable();
604
this.enabledDisposables.dispose();
605
this.disposables.dispose();
606
}
607
}
608
609
class DOMFocusController<T> implements IDisposable {
610
611
private readonly disposables = new DisposableStore();
612
613
constructor(
614
private list: List<T>,
615
private view: IListView<T>
616
) {
617
const onKeyDown = Event.chain(this.disposables.add(new DomEmitter(view.domNode, 'keydown')).event, $ => $
618
.filter(e => !isEditableElement(e.target as HTMLElement))
619
.map(e => new StandardKeyboardEvent(e))
620
);
621
622
const onTab = Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey));
623
624
onTab(this.onTab, this, this.disposables);
625
}
626
627
private onTab(e: StandardKeyboardEvent): void {
628
if (e.target !== this.view.domNode) {
629
return;
630
}
631
632
const focus = this.list.getFocus();
633
634
if (focus.length === 0) {
635
return;
636
}
637
638
const focusedDomElement = this.view.domElement(focus[0]);
639
640
if (!focusedDomElement) {
641
return;
642
}
643
644
// eslint-disable-next-line no-restricted-syntax
645
const tabIndexElement = focusedDomElement.querySelector('[tabIndex]');
646
647
if (!tabIndexElement || !(isHTMLElement(tabIndexElement)) || tabIndexElement.tabIndex === -1) {
648
return;
649
}
650
651
const style = getWindow(tabIndexElement).getComputedStyle(tabIndexElement);
652
if (style.visibility === 'hidden' || style.display === 'none') {
653
return;
654
}
655
656
e.preventDefault();
657
e.stopPropagation();
658
tabIndexElement.focus();
659
}
660
661
dispose() {
662
this.disposables.dispose();
663
}
664
}
665
666
export function isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
667
return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;
668
}
669
670
export function isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
671
return event.browserEvent.shiftKey;
672
}
673
674
function isMouseRightClick(event: UIEvent): boolean {
675
return isMouseEvent(event) && event.button === 2;
676
}
677
678
const DefaultMultipleSelectionController = {
679
isSelectionSingleChangeEvent,
680
isSelectionRangeChangeEvent
681
};
682
683
export class MouseController<T> implements IDisposable {
684
685
private multipleSelectionController: IMultipleSelectionController<T> | undefined;
686
private readonly mouseSupport: boolean;
687
private readonly disposables = new DisposableStore();
688
689
private readonly _onPointer = this.disposables.add(new Emitter<IListMouseEvent<T>>());
690
get onPointer() { return this._onPointer.event; }
691
692
constructor(protected list: List<T>) {
693
if (list.options.multipleSelectionSupport !== false) {
694
this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController;
695
}
696
697
this.mouseSupport = typeof list.options.mouseSupport === 'undefined' || !!list.options.mouseSupport;
698
699
if (this.mouseSupport) {
700
list.onMouseDown(this.onMouseDown, this, this.disposables);
701
list.onContextMenu(this.onContextMenu, this, this.disposables);
702
list.onMouseDblClick(this.onDoubleClick, this, this.disposables);
703
list.onTouchStart(this.onMouseDown, this, this.disposables);
704
this.disposables.add(Gesture.addTarget(list.getHTMLElement()));
705
}
706
707
Event.any<IListMouseEvent<any> | IListGestureEvent<any>>(list.onMouseClick, list.onMouseMiddleClick, list.onTap)(this.onViewPointer, this, this.disposables);
708
}
709
710
updateOptions(optionsUpdate: IListOptionsUpdate): void {
711
if (optionsUpdate.multipleSelectionSupport !== undefined) {
712
this.multipleSelectionController = undefined;
713
714
if (optionsUpdate.multipleSelectionSupport) {
715
this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController;
716
}
717
}
718
}
719
720
protected isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
721
if (!this.multipleSelectionController) {
722
return false;
723
}
724
725
return this.multipleSelectionController.isSelectionSingleChangeEvent(event);
726
}
727
728
protected isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
729
if (!this.multipleSelectionController) {
730
return false;
731
}
732
733
return this.multipleSelectionController.isSelectionRangeChangeEvent(event);
734
}
735
736
private isSelectionChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
737
return this.isSelectionSingleChangeEvent(event) || this.isSelectionRangeChangeEvent(event);
738
}
739
740
protected onMouseDown(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
741
if (isMonacoEditor(e.browserEvent.target as HTMLElement)) {
742
return;
743
}
744
745
if (getActiveElement() !== e.browserEvent.target) {
746
this.list.domFocus();
747
}
748
}
749
750
protected onContextMenu(e: IListContextMenuEvent<T>): void {
751
if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
752
return;
753
}
754
755
const focus = typeof e.index === 'undefined' ? [] : [e.index];
756
this.list.setFocus(focus, e.browserEvent);
757
}
758
759
protected onViewPointer(e: IListMouseEvent<T>): void {
760
if (!this.mouseSupport) {
761
return;
762
}
763
764
if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
765
return;
766
}
767
768
if (e.browserEvent.isHandledByList) {
769
return;
770
}
771
772
e.browserEvent.isHandledByList = true;
773
const focus = e.index;
774
775
if (typeof focus === 'undefined') {
776
this.list.setFocus([], e.browserEvent);
777
this.list.setSelection([], e.browserEvent);
778
this.list.setAnchor(undefined);
779
return;
780
}
781
782
if (this.isSelectionChangeEvent(e)) {
783
return this.changeSelection(e);
784
}
785
786
this.list.setFocus([focus], e.browserEvent);
787
this.list.setAnchor(focus);
788
789
if (!isMouseRightClick(e.browserEvent)) {
790
// Check if the element is selectable (getGroupId must not return undefined)
791
const focusGroupId = this.list.getElementGroupId(focus);
792
if (focusGroupId !== NotSelectableGroupId) {
793
this.list.setSelection([focus], e.browserEvent);
794
}
795
}
796
797
this._onPointer.fire(e);
798
}
799
800
protected onDoubleClick(e: IListMouseEvent<T>): void {
801
if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
802
return;
803
}
804
805
if (this.isSelectionChangeEvent(e)) {
806
return;
807
}
808
809
if (e.browserEvent.isHandledByList) {
810
return;
811
}
812
813
e.browserEvent.isHandledByList = true;
814
const focus = this.list.getFocus();
815
this.list.setSelection(focus, e.browserEvent);
816
}
817
818
private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
819
const focus = e.index!;
820
let anchor = this.list.getAnchor();
821
822
if (this.isSelectionRangeChangeEvent(e)) {
823
if (typeof anchor === 'undefined') {
824
const currentFocus = this.list.getFocus()[0];
825
anchor = currentFocus ?? focus;
826
this.list.setAnchor(anchor);
827
}
828
829
const min = Math.min(anchor, focus);
830
const max = Math.max(anchor, focus);
831
let rangeSelection = range(min, max + 1);
832
833
const selectedElement = this.list.getSelection()[0];
834
if (selectedElement !== undefined) {
835
const referenceGroupId = this.list.getElementGroupId(selectedElement);
836
if (referenceGroupId !== undefined) {
837
rangeSelection = this.list.filterIndicesByGroup(rangeSelection, referenceGroupId);
838
}
839
}
840
841
const selection = this.list.getSelection();
842
const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor);
843
844
if (contiguousRange.length === 0) {
845
return;
846
}
847
848
const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange));
849
this.list.setSelection(newSelection, e.browserEvent);
850
this.list.setFocus([focus], e.browserEvent);
851
852
} else if (this.isSelectionSingleChangeEvent(e)) {
853
const selection = this.list.getSelection();
854
const newSelection = selection.filter(i => i !== focus);
855
856
this.list.setFocus([focus]);
857
this.list.setAnchor(focus);
858
859
const focusGroupId = this.list.getElementGroupId(focus);
860
if (focusGroupId === NotSelectableGroupId) {
861
return; // Cannot select this element, do nothing
862
}
863
864
if (selection.length === newSelection.length) {
865
const itemsToBeSelected = focusGroupId !== undefined ?
866
this.list.filterIndicesByGroup([...newSelection, focus], focusGroupId)
867
: [...newSelection, focus];
868
this.list.setSelection(itemsToBeSelected, e.browserEvent);
869
} else {
870
this.list.setSelection(newSelection, e.browserEvent);
871
}
872
}
873
}
874
875
dispose() {
876
this.disposables.dispose();
877
}
878
}
879
880
export interface IMultipleSelectionController<T> {
881
isSelectionSingleChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;
882
isSelectionRangeChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;
883
}
884
885
export interface IStyleController {
886
style(styles: IListStyles): void;
887
}
888
889
export interface IListAccessibilityProvider<T> extends IListViewAccessibilityProvider<T> {
890
getAriaLabel(element: T): string | IObservable<string> | null;
891
getWidgetAriaLabel(): string | IObservable<string>;
892
getWidgetRole?(): AriaRole;
893
getAriaLevel?(element: T): number | undefined;
894
readonly onDidChangeActiveDescendant?: Event<void>;
895
getActiveDescendantId?(element: T): string | undefined;
896
}
897
898
export class DefaultStyleController implements IStyleController {
899
900
constructor(private styleElement: HTMLStyleElement, private selectorSuffix: string) { }
901
902
style(styles: IListStyles): void {
903
const suffix = this.selectorSuffix && `.${this.selectorSuffix}`;
904
const content: string[] = [];
905
906
if (styles.listBackground) {
907
content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`);
908
}
909
910
if (styles.listFocusBackground) {
911
content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`);
912
content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case!
913
}
914
915
if (styles.listFocusForeground) {
916
content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`);
917
}
918
919
if (styles.listActiveSelectionBackground) {
920
content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`);
921
content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case!
922
}
923
924
if (styles.listActiveSelectionForeground) {
925
content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`);
926
}
927
928
if (styles.listActiveSelectionIconForeground) {
929
content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; }`);
930
}
931
932
if (styles.listFocusAndSelectionBackground) {
933
content.push(`
934
.monaco-drag-image${suffix},
935
.monaco-list${suffix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }
936
`);
937
}
938
939
if (styles.listFocusAndSelectionForeground) {
940
content.push(`
941
.monaco-drag-image${suffix},
942
.monaco-list${suffix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }
943
`);
944
}
945
946
if (styles.listInactiveFocusForeground) {
947
content.push(`.monaco-list${suffix} .monaco-list-row.focused { color: ${styles.listInactiveFocusForeground}; }`);
948
content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { color: ${styles.listInactiveFocusForeground}; }`); // overwrite :hover style in this case!
949
}
950
951
if (styles.listInactiveSelectionIconForeground) {
952
content.push(`.monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; }`);
953
}
954
955
if (styles.listInactiveFocusBackground) {
956
content.push(`.monaco-list${suffix} .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`);
957
content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!
958
}
959
960
if (styles.listInactiveSelectionBackground) {
961
content.push(`.monaco-list${suffix} .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`);
962
content.push(`.monaco-list${suffix} .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case!
963
}
964
965
if (styles.listInactiveSelectionForeground) {
966
content.push(`.monaco-list${suffix} .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`);
967
}
968
969
if (styles.listHoverBackground) {
970
content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);
971
}
972
973
if (styles.listHoverForeground) {
974
content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`);
975
}
976
977
/**
978
* Outlines
979
*/
980
const focusAndSelectionOutline = asCssValueWithDefault(styles.listFocusAndSelectionOutline, asCssValueWithDefault(styles.listSelectionOutline, styles.listFocusOutline ?? ''));
981
if (focusAndSelectionOutline) { // default: listFocusOutline
982
content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused.selected { outline: 1px solid ${focusAndSelectionOutline}; outline-offset: -1px;}`);
983
}
984
985
if (styles.listFocusOutline) { // default: set
986
content.push(`
987
.monaco-drag-image${suffix},
988
.monaco-list${suffix}:focus .monaco-list-row.focused,
989
.context-menu-visible .monaco-list${suffix}.last-focused .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }
990
`);
991
}
992
993
const inactiveFocusAndSelectionOutline = asCssValueWithDefault(styles.listSelectionOutline, styles.listInactiveFocusOutline ?? '');
994
if (inactiveFocusAndSelectionOutline) {
995
content.push(`.monaco-list${suffix} .monaco-list-row.focused.selected { outline: 1px dotted ${inactiveFocusAndSelectionOutline}; outline-offset: -1px; }`);
996
}
997
998
if (styles.listSelectionOutline) { // default: activeContrastBorder
999
content.push(`.monaco-list${suffix} .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`);
1000
}
1001
1002
if (styles.listInactiveFocusOutline) { // default: null
1003
content.push(`.monaco-list${suffix} .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`);
1004
}
1005
1006
if (styles.listHoverOutline) { // default: activeContrastBorder
1007
content.push(`.monaco-list${suffix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);
1008
}
1009
1010
if (styles.listDropOverBackground) {
1011
content.push(`
1012
.monaco-list${suffix}.drop-target,
1013
.monaco-list${suffix} .monaco-list-rows.drop-target,
1014
.monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropOverBackground} !important; color: inherit !important; }
1015
`);
1016
}
1017
1018
if (styles.listDropBetweenBackground) {
1019
content.push(`
1020
.monaco-list${suffix} .monaco-list-rows.drop-target-before .monaco-list-row:first-child::before,
1021
.monaco-list${suffix} .monaco-list-row.drop-target-before::before {
1022
content: ""; position: absolute; top: 0px; left: 0px; width: 100%; height: 1px;
1023
background-color: ${styles.listDropBetweenBackground};
1024
}`);
1025
content.push(`
1026
.monaco-list${suffix} .monaco-list-rows.drop-target-after .monaco-list-row:last-child::after,
1027
.monaco-list${suffix} .monaco-list-row.drop-target-after::after {
1028
content: ""; position: absolute; bottom: 0px; left: 0px; width: 100%; height: 1px;
1029
background-color: ${styles.listDropBetweenBackground};
1030
}`);
1031
}
1032
1033
if (styles.tableColumnsBorder) {
1034
content.push(`
1035
.monaco-table > .monaco-split-view2,
1036
.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before,
1037
.monaco-enable-motion .monaco-table:hover > .monaco-split-view2,
1038
.monaco-enable-motion .monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before {
1039
border-color: ${styles.tableColumnsBorder};
1040
}
1041
1042
.monaco-enable-motion .monaco-table > .monaco-split-view2,
1043
.monaco-enable-motion .monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {
1044
border-color: transparent;
1045
}
1046
`);
1047
}
1048
1049
if (styles.tableOddRowsBackgroundColor) {
1050
content.push(`
1051
.monaco-table .monaco-list-row[data-parity=odd]:not(.focused):not(.selected):not(:hover) .monaco-table-tr,
1052
.monaco-table .monaco-list:not(:focus) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr,
1053
.monaco-table .monaco-list:not(.focused) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr {
1054
background-color: ${styles.tableOddRowsBackgroundColor};
1055
}
1056
`);
1057
}
1058
1059
this.styleElement.textContent = content.join('\n');
1060
}
1061
}
1062
1063
export interface IKeyboardNavigationEventFilter {
1064
(e: StandardKeyboardEvent): boolean;
1065
}
1066
1067
export interface IListOptionsUpdate extends IListViewOptionsUpdate {
1068
readonly typeNavigationEnabled?: boolean;
1069
readonly typeNavigationMode?: TypeNavigationMode;
1070
readonly multipleSelectionSupport?: boolean;
1071
}
1072
1073
export interface IListOptions<T> extends IListOptionsUpdate {
1074
readonly identityProvider?: IIdentityProvider<T>;
1075
readonly dnd?: IListDragAndDrop<T>;
1076
readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider<T>;
1077
readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate;
1078
readonly keyboardSupport?: boolean;
1079
readonly multipleSelectionController?: IMultipleSelectionController<T>;
1080
readonly styleController?: (suffix: string) => IStyleController;
1081
readonly accessibilityProvider?: IListAccessibilityProvider<T>;
1082
readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
1083
1084
// list view options
1085
readonly useShadows?: boolean;
1086
readonly verticalScrollMode?: ScrollbarVisibility;
1087
readonly setRowLineHeight?: boolean;
1088
readonly setRowHeight?: boolean;
1089
readonly supportDynamicHeights?: boolean;
1090
readonly mouseSupport?: boolean;
1091
readonly userSelection?: boolean;
1092
readonly horizontalScrolling?: boolean;
1093
readonly scrollByPage?: boolean;
1094
readonly transformOptimization?: boolean;
1095
readonly smoothScrolling?: boolean;
1096
readonly scrollableElementChangeOptions?: ScrollableElementChangeOptions;
1097
readonly alwaysConsumeMouseWheel?: boolean;
1098
readonly initialSize?: Dimension;
1099
readonly paddingTop?: number;
1100
readonly paddingBottom?: number;
1101
}
1102
1103
export interface IListStyles {
1104
listBackground: string | undefined;
1105
listFocusBackground: string | undefined;
1106
listFocusForeground: string | undefined;
1107
listActiveSelectionBackground: string | undefined;
1108
listActiveSelectionForeground: string | undefined;
1109
listActiveSelectionIconForeground: string | undefined;
1110
listFocusAndSelectionOutline: string | undefined;
1111
listFocusAndSelectionBackground: string | undefined;
1112
listFocusAndSelectionForeground: string | undefined;
1113
listInactiveSelectionBackground: string | undefined;
1114
listInactiveSelectionIconForeground: string | undefined;
1115
listInactiveSelectionForeground: string | undefined;
1116
listInactiveFocusForeground: string | undefined;
1117
listInactiveFocusBackground: string | undefined;
1118
listHoverBackground: string | undefined;
1119
listHoverForeground: string | undefined;
1120
listDropOverBackground: string | undefined;
1121
listDropBetweenBackground: string | undefined;
1122
listFocusOutline: string | undefined;
1123
listInactiveFocusOutline: string | undefined;
1124
listSelectionOutline: string | undefined;
1125
listHoverOutline: string | undefined;
1126
treeIndentGuidesStroke: string | undefined;
1127
treeInactiveIndentGuidesStroke: string | undefined;
1128
treeStickyScrollBackground: string | undefined;
1129
treeStickyScrollBorder: string | undefined;
1130
treeStickyScrollShadow: string | undefined;
1131
tableColumnsBorder: string | undefined;
1132
tableOddRowsBackgroundColor: string | undefined;
1133
}
1134
1135
export const unthemedListStyles: IListStyles = {
1136
listFocusBackground: '#7FB0D0',
1137
listActiveSelectionBackground: '#0E639C',
1138
listActiveSelectionForeground: '#FFFFFF',
1139
listActiveSelectionIconForeground: '#FFFFFF',
1140
listFocusAndSelectionOutline: '#90C2F9',
1141
listFocusAndSelectionBackground: '#094771',
1142
listFocusAndSelectionForeground: '#FFFFFF',
1143
listInactiveSelectionBackground: '#3F3F46',
1144
listInactiveSelectionIconForeground: '#FFFFFF',
1145
listHoverBackground: '#2A2D2E',
1146
listDropOverBackground: '#383B3D',
1147
listDropBetweenBackground: '#EEEEEE',
1148
treeIndentGuidesStroke: '#a9a9a9',
1149
treeInactiveIndentGuidesStroke: Color.fromHex('#a9a9a9').transparent(0.4).toString(),
1150
tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2).toString(),
1151
tableOddRowsBackgroundColor: Color.fromHex('#cccccc').transparent(0.04).toString(),
1152
listBackground: undefined,
1153
listFocusForeground: undefined,
1154
listInactiveSelectionForeground: undefined,
1155
listInactiveFocusForeground: undefined,
1156
listInactiveFocusBackground: undefined,
1157
listHoverForeground: undefined,
1158
listFocusOutline: undefined,
1159
listInactiveFocusOutline: undefined,
1160
listSelectionOutline: undefined,
1161
listHoverOutline: undefined,
1162
treeStickyScrollBackground: undefined,
1163
treeStickyScrollBorder: undefined,
1164
treeStickyScrollShadow: undefined
1165
};
1166
1167
const DefaultOptions: IListOptions<any> = {
1168
keyboardSupport: true,
1169
mouseSupport: true,
1170
multipleSelectionSupport: true,
1171
dnd: {
1172
getDragURI() { return null; },
1173
onDragStart(): void { },
1174
onDragOver() { return false; },
1175
drop() { },
1176
dispose() { }
1177
}
1178
};
1179
1180
// TODO@Joao: move these utils into a SortedArray class
1181
1182
function getContiguousRangeContaining(range: number[], value: number): number[] {
1183
const index = range.indexOf(value);
1184
1185
if (index === -1) {
1186
return [];
1187
}
1188
1189
const result: number[] = [];
1190
let i = index - 1;
1191
while (i >= 0 && range[i] === value - (index - i)) {
1192
result.push(range[i--]);
1193
}
1194
1195
result.reverse();
1196
i = index;
1197
while (i < range.length && range[i] === value + (i - index)) {
1198
result.push(range[i++]);
1199
}
1200
1201
return result;
1202
}
1203
1204
/**
1205
* Given two sorted collections of numbers, returns the intersection
1206
* between them (OR).
1207
*/
1208
function disjunction(one: number[], other: number[]): number[] {
1209
const result: number[] = [];
1210
let i = 0, j = 0;
1211
1212
while (i < one.length || j < other.length) {
1213
if (i >= one.length) {
1214
result.push(other[j++]);
1215
} else if (j >= other.length) {
1216
result.push(one[i++]);
1217
} else if (one[i] === other[j]) {
1218
result.push(one[i]);
1219
i++;
1220
j++;
1221
continue;
1222
} else if (one[i] < other[j]) {
1223
result.push(one[i++]);
1224
} else {
1225
result.push(other[j++]);
1226
}
1227
}
1228
1229
return result;
1230
}
1231
1232
/**
1233
* Given two sorted collections of numbers, returns the relative
1234
* complement between them (XOR).
1235
*/
1236
function relativeComplement(one: number[], other: number[]): number[] {
1237
const result: number[] = [];
1238
let i = 0, j = 0;
1239
1240
while (i < one.length || j < other.length) {
1241
if (i >= one.length) {
1242
result.push(other[j++]);
1243
} else if (j >= other.length) {
1244
result.push(one[i++]);
1245
} else if (one[i] === other[j]) {
1246
i++;
1247
j++;
1248
continue;
1249
} else if (one[i] < other[j]) {
1250
result.push(one[i++]);
1251
} else {
1252
j++;
1253
}
1254
}
1255
1256
return result;
1257
}
1258
1259
const numericSort = (a: number, b: number) => a - b;
1260
1261
class PipelineRenderer<T> implements IListRenderer<T, any> {
1262
1263
constructor(
1264
private _templateId: string,
1265
private renderers: IListRenderer<any /* TODO@joao */, any>[]
1266
) { }
1267
1268
get templateId(): string {
1269
return this._templateId;
1270
}
1271
1272
renderTemplate(container: HTMLElement): any[] {
1273
return this.renderers.map(r => r.renderTemplate(container));
1274
}
1275
1276
renderElement(element: T, index: number, templateData: any[], renderDetails?: IListElementRenderDetails): void {
1277
let i = 0;
1278
1279
for (const renderer of this.renderers) {
1280
renderer.renderElement(element, index, templateData[i++], renderDetails);
1281
}
1282
}
1283
1284
disposeElement(element: T, index: number, templateData: any[], renderDetails?: IListElementRenderDetails): void {
1285
let i = 0;
1286
1287
for (const renderer of this.renderers) {
1288
renderer.disposeElement?.(element, index, templateData[i], renderDetails);
1289
1290
i += 1;
1291
}
1292
}
1293
1294
disposeTemplate(templateData: unknown[]): void {
1295
let i = 0;
1296
1297
for (const renderer of this.renderers) {
1298
renderer.disposeTemplate(templateData[i++]);
1299
}
1300
}
1301
}
1302
1303
class AccessibiltyRenderer<T> implements IListRenderer<T, IAccessibilityTemplateData> {
1304
1305
templateId: string = 'a18n';
1306
1307
constructor(private accessibilityProvider: IListAccessibilityProvider<T>) { }
1308
1309
renderTemplate(container: HTMLElement): IAccessibilityTemplateData {
1310
return { container, disposables: new DisposableStore() };
1311
}
1312
1313
renderElement(element: T, index: number, data: IAccessibilityTemplateData): void {
1314
const ariaLabel = this.accessibilityProvider.getAriaLabel(element);
1315
const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel);
1316
1317
data.disposables.add(autorun(reader => {
1318
this.setAriaLabel(reader.readObservable(observable), data.container);
1319
}));
1320
1321
const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element);
1322
1323
if (typeof ariaLevel === 'number') {
1324
data.container.setAttribute('aria-level', `${ariaLevel}`);
1325
} else {
1326
data.container.removeAttribute('aria-level');
1327
}
1328
}
1329
1330
private setAriaLabel(ariaLabel: string | null, element: HTMLElement): void {
1331
if (ariaLabel) {
1332
element.setAttribute('aria-label', ariaLabel);
1333
} else {
1334
element.removeAttribute('aria-label');
1335
}
1336
}
1337
1338
disposeElement(element: T, index: number, templateData: IAccessibilityTemplateData): void {
1339
templateData.disposables.clear();
1340
}
1341
1342
disposeTemplate(templateData: IAccessibilityTemplateData): void {
1343
templateData.disposables.dispose();
1344
}
1345
}
1346
1347
class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
1348
1349
constructor(private list: List<T>, private dnd: IListDragAndDrop<T>) { }
1350
1351
getDragElements(element: T): T[] {
1352
const selection = this.list.getSelectedElements();
1353
const elements = selection.indexOf(element) > -1 ? selection : [element];
1354
return elements;
1355
}
1356
1357
getDragURI(element: T): string | null {
1358
return this.dnd.getDragURI(element);
1359
}
1360
1361
getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined {
1362
if (this.dnd.getDragLabel) {
1363
return this.dnd.getDragLabel(elements, originalEvent);
1364
}
1365
1366
return undefined;
1367
}
1368
1369
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
1370
this.dnd.onDragStart?.(data, originalEvent);
1371
}
1372
1373
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction {
1374
return this.dnd.onDragOver(data, targetElement, targetIndex, targetSector, originalEvent);
1375
}
1376
1377
onDragLeave(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
1378
this.dnd.onDragLeave?.(data, targetElement, targetIndex, originalEvent);
1379
}
1380
1381
onDragEnd(originalEvent: DragEvent): void {
1382
this.dnd.onDragEnd?.(originalEvent);
1383
}
1384
1385
drop(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void {
1386
this.dnd.drop(data, targetElement, targetIndex, targetSector, originalEvent);
1387
}
1388
1389
dispose(): void {
1390
this.dnd.dispose();
1391
}
1392
}
1393
1394
/**
1395
* The {@link List} is a virtual scrolling widget, built on top of the {@link ListView}
1396
* widget.
1397
*
1398
* Features:
1399
* - Customizable keyboard and mouse support
1400
* - Element traits: focus, selection, achor
1401
* - Accessibility support
1402
* - Touch support
1403
* - Performant template-based rendering
1404
* - Horizontal scrolling
1405
* - Variable element height support
1406
* - Dynamic element height support
1407
* - Drag-and-drop support
1408
*/
1409
export class List<T> implements ISpliceable<T>, IDisposable {
1410
1411
private focus = new Trait<T>('focused');
1412
private selection: Trait<T>;
1413
private anchor = new Trait<T>('anchor');
1414
private eventBufferer = new EventBufferer();
1415
protected view: IListView<T>;
1416
private spliceable: ISpliceable<T>;
1417
private styleController: IStyleController;
1418
private typeNavigationController?: TypeNavigationController<T>;
1419
private accessibilityProvider?: IListAccessibilityProvider<T>;
1420
private keyboardController: KeyboardController<T> | undefined;
1421
private mouseController: MouseController<T>;
1422
private _ariaLabel: string = '';
1423
1424
protected readonly disposables = new DisposableStore();
1425
1426
@memoize get onDidChangeFocus(): Event<IListEvent<T>> {
1427
return Event.map(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toListEvent(e), this.disposables);
1428
}
1429
1430
@memoize get onDidChangeSelection(): Event<IListEvent<T>> {
1431
return Event.map(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toListEvent(e), this.disposables);
1432
}
1433
1434
get domId(): string { return this.view.domId; }
1435
get onDidScroll(): Event<ScrollEvent> { return this.view.onDidScroll; }
1436
get onMouseClick(): Event<IListMouseEvent<T>> { return this.view.onMouseClick; }
1437
get onMouseDblClick(): Event<IListMouseEvent<T>> { return this.view.onMouseDblClick; }
1438
get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return this.view.onMouseMiddleClick; }
1439
get onPointer(): Event<IListMouseEvent<T>> { return this.mouseController.onPointer; }
1440
get onMouseUp(): Event<IListMouseEvent<T>> { return this.view.onMouseUp; }
1441
get onMouseDown(): Event<IListMouseEvent<T>> { return this.view.onMouseDown; }
1442
get onMouseOver(): Event<IListMouseEvent<T>> { return this.view.onMouseOver; }
1443
get onMouseMove(): Event<IListMouseEvent<T>> { return this.view.onMouseMove; }
1444
get onMouseOut(): Event<IListMouseEvent<T>> { return this.view.onMouseOut; }
1445
get onTouchStart(): Event<IListTouchEvent<T>> { return this.view.onTouchStart; }
1446
get onTap(): Event<IListGestureEvent<T>> { return this.view.onTap; }
1447
1448
/**
1449
* Possible context menu trigger events:
1450
* - ContextMenu key
1451
* - Shift F10
1452
* - Ctrl Option Shift M (macOS with VoiceOver)
1453
* - Mouse right click
1454
*/
1455
@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
1456
let didJustPressContextMenuKey = false;
1457
1458
const fromKeyDown: Event<any> = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>
1459
$.map(e => new StandardKeyboardEvent(e))
1460
.filter(e => didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
1461
.map(e => EventHelper.stop(e, true))
1462
.filter(() => false));
1463
1464
const fromKeyUp = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event, $ =>
1465
$.forEach(() => didJustPressContextMenuKey = false)
1466
.map(e => new StandardKeyboardEvent(e))
1467
.filter(e => e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
1468
.map(e => EventHelper.stop(e, true))
1469
.map(({ browserEvent }) => {
1470
const focus = this.getFocus();
1471
const index = focus.length ? focus[0] : undefined;
1472
const element = typeof index !== 'undefined' ? this.view.element(index) : undefined;
1473
const anchor = typeof index !== 'undefined' ? this.view.domElement(index) as HTMLElement : this.view.domNode;
1474
return { index, element, anchor, browserEvent };
1475
}));
1476
1477
const fromMouse = Event.chain(this.view.onContextMenu, $ =>
1478
$.filter(_ => !didJustPressContextMenuKey)
1479
.map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(getWindow(this.view.domNode), browserEvent), browserEvent }))
1480
);
1481
1482
return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);
1483
}
1484
1485
@memoize get onKeyDown(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event; }
1486
@memoize get onKeyUp(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event; }
1487
@memoize get onKeyPress(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keypress')).event; }
1488
1489
@memoize get onDidFocus(): Event<void> { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'focus', true)).event); }
1490
@memoize get onDidBlur(): Event<void> { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'blur', true)).event); }
1491
1492
private readonly _onDidDispose = new Emitter<void>();
1493
readonly onDidDispose: Event<void> = this._onDidDispose.event;
1494
1495
constructor(
1496
private user: string,
1497
container: HTMLElement,
1498
virtualDelegate: IListVirtualDelegate<T>,
1499
renderers: IListRenderer<any /* TODO@joao */, any>[],
1500
private _options: IListOptions<T> = DefaultOptions
1501
) {
1502
const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list';
1503
this.selection = new SelectionTrait(role !== 'listbox');
1504
1505
const baseRenderers: IListRenderer<T, unknown>[] = [this.focus.renderer, this.selection.renderer];
1506
1507
this.accessibilityProvider = _options.accessibilityProvider;
1508
1509
if (this.accessibilityProvider) {
1510
baseRenderers.push(new AccessibiltyRenderer<T>(this.accessibilityProvider));
1511
1512
this.accessibilityProvider.onDidChangeActiveDescendant?.(this.onDidChangeActiveDescendant, this, this.disposables);
1513
}
1514
1515
renderers = renderers.map(r => new PipelineRenderer(r.templateId, [...baseRenderers, r]));
1516
1517
const viewOptions: IListViewOptions<T> = {
1518
..._options,
1519
dnd: _options.dnd && new ListViewDragAndDrop(this, _options.dnd)
1520
};
1521
1522
this.view = this.createListView(container, virtualDelegate, renderers, viewOptions);
1523
this.view.domNode.setAttribute('role', role);
1524
1525
if (_options.styleController) {
1526
this.styleController = _options.styleController(this.view.domId);
1527
} else {
1528
const styleElement = createStyleSheet(this.view.domNode);
1529
this.styleController = new DefaultStyleController(styleElement, this.view.domId);
1530
}
1531
1532
this.spliceable = new CombinedSpliceable([
1533
new TraitSpliceable(this.focus, this.view, _options.identityProvider),
1534
new TraitSpliceable(this.selection, this.view, _options.identityProvider),
1535
new TraitSpliceable(this.anchor, this.view, _options.identityProvider),
1536
this.view
1537
]);
1538
1539
this.disposables.add(this.focus);
1540
this.disposables.add(this.selection);
1541
this.disposables.add(this.anchor);
1542
this.disposables.add(this.view);
1543
this.disposables.add(this._onDidDispose);
1544
1545
this.disposables.add(new DOMFocusController(this, this.view));
1546
1547
if (typeof _options.keyboardSupport !== 'boolean' || _options.keyboardSupport) {
1548
this.keyboardController = new KeyboardController(this, this.view, _options);
1549
this.disposables.add(this.keyboardController);
1550
}
1551
1552
if (_options.keyboardNavigationLabelProvider) {
1553
const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;
1554
this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, _options.keyboardNavigationEventFilter ?? (() => true), delegate);
1555
this.disposables.add(this.typeNavigationController);
1556
}
1557
1558
this.mouseController = this.createMouseController(_options);
1559
this.disposables.add(this.mouseController);
1560
1561
this.onDidChangeFocus(this._onFocusChange, this, this.disposables);
1562
this.onDidChangeSelection(this._onSelectionChange, this, this.disposables);
1563
1564
if (this.accessibilityProvider) {
1565
const ariaLabel = this.accessibilityProvider.getWidgetAriaLabel();
1566
const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel);
1567
1568
this.disposables.add(autorun(reader => {
1569
this.ariaLabel = reader.readObservable(observable);
1570
}));
1571
}
1572
1573
if (this._options.multipleSelectionSupport !== false) {
1574
this.view.domNode.setAttribute('aria-multiselectable', 'true');
1575
}
1576
}
1577
1578
protected createListView(container: HTMLElement, virtualDelegate: IListVirtualDelegate<T>, renderers: IListRenderer<any, any>[], viewOptions: IListViewOptions<T>): IListView<T> {
1579
return new ListView(container, virtualDelegate, renderers, viewOptions);
1580
}
1581
1582
protected createMouseController(options: IListOptions<T>): MouseController<T> {
1583
return new MouseController(this);
1584
}
1585
1586
updateOptions(optionsUpdate: IListOptionsUpdate = {}): void {
1587
this._options = { ...this._options, ...optionsUpdate };
1588
1589
this.typeNavigationController?.updateOptions(this._options);
1590
1591
if (this._options.multipleSelectionController !== undefined) {
1592
if (this._options.multipleSelectionSupport) {
1593
this.view.domNode.setAttribute('aria-multiselectable', 'true');
1594
} else {
1595
this.view.domNode.removeAttribute('aria-multiselectable');
1596
}
1597
}
1598
1599
this.mouseController.updateOptions(optionsUpdate);
1600
this.keyboardController?.updateOptions(optionsUpdate);
1601
this.view.updateOptions(optionsUpdate);
1602
}
1603
1604
get options(): IListOptions<T> {
1605
return this._options;
1606
}
1607
1608
splice(start: number, deleteCount: number, elements: readonly T[] = []): void {
1609
if (start < 0 || start > this.view.length) {
1610
throw new ListError(this.user, `Invalid start index: ${start}`);
1611
}
1612
1613
if (deleteCount < 0) {
1614
throw new ListError(this.user, `Invalid delete count: ${deleteCount}`);
1615
}
1616
1617
if (deleteCount === 0 && elements.length === 0) {
1618
return;
1619
}
1620
1621
this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements));
1622
}
1623
1624
updateWidth(index: number): void {
1625
this.view.updateWidth(index);
1626
}
1627
1628
updateElementHeight(index: number, size: number | undefined): void {
1629
this.view.updateElementHeight(index, size, null);
1630
}
1631
1632
rerender(): void {
1633
this.view.rerender();
1634
}
1635
1636
element(index: number): T {
1637
return this.view.element(index);
1638
}
1639
1640
indexOf(element: T): number {
1641
return this.view.indexOf(element);
1642
}
1643
1644
indexAt(position: number): number {
1645
return this.view.indexAt(position);
1646
}
1647
1648
get length(): number {
1649
return this.view.length;
1650
}
1651
1652
get contentHeight(): number {
1653
return this.view.contentHeight;
1654
}
1655
1656
get contentWidth(): number {
1657
return this.view.contentWidth;
1658
}
1659
1660
get onDidChangeContentHeight(): Event<number> {
1661
return this.view.onDidChangeContentHeight;
1662
}
1663
1664
get onDidChangeContentWidth(): Event<number> {
1665
return this.view.onDidChangeContentWidth;
1666
}
1667
1668
get scrollTop(): number {
1669
return this.view.getScrollTop();
1670
}
1671
1672
set scrollTop(scrollTop: number) {
1673
this.view.setScrollTop(scrollTop);
1674
}
1675
1676
get scrollLeft(): number {
1677
return this.view.getScrollLeft();
1678
}
1679
1680
set scrollLeft(scrollLeft: number) {
1681
this.view.setScrollLeft(scrollLeft);
1682
}
1683
1684
get scrollHeight(): number {
1685
return this.view.scrollHeight;
1686
}
1687
1688
get renderHeight(): number {
1689
return this.view.renderHeight;
1690
}
1691
1692
get firstVisibleIndex(): number {
1693
return this.view.firstVisibleIndex;
1694
}
1695
1696
get firstMostlyVisibleIndex(): number {
1697
return this.view.firstMostlyVisibleIndex;
1698
}
1699
1700
get lastVisibleIndex(): number {
1701
return this.view.lastVisibleIndex;
1702
}
1703
1704
get ariaLabel(): string {
1705
return this._ariaLabel;
1706
}
1707
1708
set ariaLabel(value: string) {
1709
this._ariaLabel = value;
1710
this.view.domNode.setAttribute('aria-label', value);
1711
}
1712
1713
domFocus(): void {
1714
this.view.domNode.focus({ preventScroll: true });
1715
}
1716
1717
layout(height?: number, width?: number): void {
1718
this.view.layout(height, width);
1719
}
1720
1721
triggerTypeNavigation(): void {
1722
this.typeNavigationController?.trigger();
1723
}
1724
1725
setSelection(indexes: number[], browserEvent?: UIEvent): void {
1726
for (const index of indexes) {
1727
if (index < 0 || index >= this.length) {
1728
throw new ListError(this.user, `Invalid index ${index}`);
1729
}
1730
}
1731
1732
indexes = indexes.filter(i => this.getElementGroupId(i) !== NotSelectableGroupId);
1733
1734
this.selection.set(indexes, browserEvent);
1735
}
1736
1737
getSelection(): number[] {
1738
return this.selection.get();
1739
}
1740
1741
getSelectedElements(): T[] {
1742
return this.getSelection().map(i => this.view.element(i));
1743
}
1744
1745
setAnchor(index: number | undefined): void {
1746
if (typeof index === 'undefined') {
1747
this.anchor.set([]);
1748
return;
1749
}
1750
1751
if (index < 0 || index >= this.length) {
1752
throw new ListError(this.user, `Invalid index ${index}`);
1753
}
1754
1755
this.anchor.set([index]);
1756
}
1757
1758
getAnchor(): number | undefined {
1759
return this.anchor.get().at(0);
1760
}
1761
1762
getAnchorElement(): T | undefined {
1763
const anchor = this.getAnchor();
1764
return typeof anchor === 'undefined' ? undefined : this.element(anchor);
1765
}
1766
1767
/**
1768
* Gets the group ID for an element at the given index.
1769
* Returns undefined if no identity provider, no getGroupId method, or if the group ID is undefined.
1770
*/
1771
getElementGroupId(index: number): number | NotSelectableGroupIdType | undefined {
1772
const identityProvider = this.options.identityProvider;
1773
if (!identityProvider?.getGroupId) {
1774
return undefined;
1775
}
1776
1777
const element = this.element(index);
1778
return identityProvider.getGroupId(element);
1779
}
1780
1781
/**
1782
* Filters the given indices to only include those with a matching group ID.
1783
* If no identity provider or getGroupId method exists, returns the original indices.
1784
* If referenceGroupId is undefined, returns an empty array (elements without group IDs are not selectable).
1785
*/
1786
filterIndicesByGroup(indices: number[], referenceGroupId: number | NotSelectableGroupIdType): number[] {
1787
const identityProvider = this.options.identityProvider;
1788
if (!identityProvider?.getGroupId) {
1789
return indices;
1790
}
1791
1792
if (referenceGroupId === NotSelectableGroupId) {
1793
return [];
1794
}
1795
1796
return indices.filter(index => {
1797
const element = this.element(index);
1798
const groupId = identityProvider.getGroupId!(element);
1799
return groupId === referenceGroupId;
1800
});
1801
}
1802
1803
setFocus(indexes: number[], browserEvent?: UIEvent): void {
1804
for (const index of indexes) {
1805
if (index < 0 || index >= this.length) {
1806
throw new ListError(this.user, `Invalid index ${index}`);
1807
}
1808
}
1809
1810
this.focus.set(indexes, browserEvent);
1811
}
1812
1813
focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1814
if (this.length === 0) { return; }
1815
1816
const focus = this.focus.get();
1817
const index = this.findNextIndex(focus.length > 0 ? focus[0] + n : 0, loop, filter);
1818
1819
if (index > -1) {
1820
this.setFocus([index], browserEvent);
1821
}
1822
}
1823
1824
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1825
if (this.length === 0) { return; }
1826
1827
const focus = this.focus.get();
1828
const index = this.findPreviousIndex(focus.length > 0 ? focus[0] - n : 0, loop, filter);
1829
1830
if (index > -1) {
1831
this.setFocus([index], browserEvent);
1832
}
1833
}
1834
1835
async focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): Promise<void> {
1836
let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight);
1837
lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;
1838
const currentlyFocusedElementIndex = this.getFocus()[0];
1839
1840
if (currentlyFocusedElementIndex !== lastPageIndex && (currentlyFocusedElementIndex === undefined || lastPageIndex > currentlyFocusedElementIndex)) {
1841
const lastGoodPageIndex = this.findPreviousIndex(lastPageIndex, false, filter);
1842
1843
if (lastGoodPageIndex > -1 && currentlyFocusedElementIndex !== lastGoodPageIndex) {
1844
this.setFocus([lastGoodPageIndex], browserEvent);
1845
} else {
1846
this.setFocus([lastPageIndex], browserEvent);
1847
}
1848
} else {
1849
const previousScrollTop = this.view.getScrollTop();
1850
let nextpageScrollTop = previousScrollTop + this.view.renderHeight;
1851
if (lastPageIndex > currentlyFocusedElementIndex) {
1852
// scroll last page element to the top only if the last page element is below the focused element
1853
nextpageScrollTop -= this.view.elementHeight(lastPageIndex);
1854
}
1855
1856
this.view.setScrollTop(nextpageScrollTop);
1857
1858
if (this.view.getScrollTop() !== previousScrollTop) {
1859
this.setFocus([]);
1860
1861
// Let the scroll event listener run
1862
await timeout(0);
1863
await this.focusNextPage(browserEvent, filter);
1864
}
1865
}
1866
}
1867
1868
async focusPreviousPage(browserEvent?: UIEvent, filter?: (element: T) => boolean, getPaddingTop: () => number = () => 0): Promise<void> {
1869
let firstPageIndex: number;
1870
const paddingTop = getPaddingTop();
1871
const scrollTop = this.view.getScrollTop() + paddingTop;
1872
1873
if (scrollTop === 0) {
1874
firstPageIndex = this.view.indexAt(scrollTop);
1875
} else {
1876
firstPageIndex = this.view.indexAfter(scrollTop - 1);
1877
}
1878
1879
const currentlyFocusedElementIndex = this.getFocus()[0];
1880
1881
if (currentlyFocusedElementIndex !== firstPageIndex && (currentlyFocusedElementIndex === undefined || currentlyFocusedElementIndex >= firstPageIndex)) {
1882
const firstGoodPageIndex = this.findNextIndex(firstPageIndex, false, filter);
1883
1884
if (firstGoodPageIndex > -1 && currentlyFocusedElementIndex !== firstGoodPageIndex) {
1885
this.setFocus([firstGoodPageIndex], browserEvent);
1886
} else {
1887
this.setFocus([firstPageIndex], browserEvent);
1888
}
1889
} else {
1890
const previousScrollTop = scrollTop;
1891
this.view.setScrollTop(scrollTop - this.view.renderHeight - paddingTop);
1892
1893
if (this.view.getScrollTop() + getPaddingTop() !== previousScrollTop) {
1894
this.setFocus([]);
1895
1896
// Let the scroll event listener run
1897
await timeout(0);
1898
await this.focusPreviousPage(browserEvent, filter, getPaddingTop);
1899
}
1900
}
1901
}
1902
1903
focusLast(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1904
if (this.length === 0) { return; }
1905
1906
const index = this.findPreviousIndex(this.length - 1, false, filter);
1907
1908
if (index > -1) {
1909
this.setFocus([index], browserEvent);
1910
}
1911
}
1912
1913
focusFirst(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1914
this.focusNth(0, browserEvent, filter);
1915
}
1916
1917
focusNth(n: number, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
1918
if (this.length === 0) { return; }
1919
1920
const index = this.findNextIndex(n, false, filter);
1921
1922
if (index > -1) {
1923
this.setFocus([index], browserEvent);
1924
}
1925
}
1926
1927
private findNextIndex(index: number, loop = false, filter?: (element: T) => boolean): number {
1928
for (let i = 0; i < this.length; i++) {
1929
if (index >= this.length && !loop) {
1930
return -1;
1931
}
1932
1933
index = index % this.length;
1934
1935
if (!filter || filter(this.element(index))) {
1936
return index;
1937
}
1938
1939
index++;
1940
}
1941
1942
return -1;
1943
}
1944
1945
private findPreviousIndex(index: number, loop = false, filter?: (element: T) => boolean): number {
1946
for (let i = 0; i < this.length; i++) {
1947
if (index < 0 && !loop) {
1948
return -1;
1949
}
1950
1951
index = (this.length + (index % this.length)) % this.length;
1952
1953
if (!filter || filter(this.element(index))) {
1954
return index;
1955
}
1956
1957
index--;
1958
}
1959
1960
return -1;
1961
}
1962
1963
getFocus(): number[] {
1964
return this.focus.get();
1965
}
1966
1967
getFocusedElements(): T[] {
1968
return this.getFocus().map(i => this.view.element(i));
1969
}
1970
1971
reveal(index: number, relativeTop?: number, paddingTop: number = 0): void {
1972
if (index < 0 || index >= this.length) {
1973
throw new ListError(this.user, `Invalid index ${index}`);
1974
}
1975
1976
const scrollTop = this.view.getScrollTop();
1977
const elementTop = this.view.elementTop(index);
1978
const elementHeight = this.view.elementHeight(index);
1979
1980
if (isNumber(relativeTop)) {
1981
// y = mx + b
1982
const m = elementHeight - this.view.renderHeight + paddingTop;
1983
this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop - paddingTop);
1984
} else {
1985
const viewItemBottom = elementTop + elementHeight;
1986
const scrollBottom = scrollTop + this.view.renderHeight;
1987
1988
if (elementTop < scrollTop + paddingTop && viewItemBottom >= scrollBottom) {
1989
// The element is already overflowing the viewport, no-op
1990
} else if (elementTop < scrollTop + paddingTop || (viewItemBottom >= scrollBottom && elementHeight >= this.view.renderHeight)) {
1991
this.view.setScrollTop(elementTop - paddingTop);
1992
} else if (viewItemBottom >= scrollBottom) {
1993
this.view.setScrollTop(viewItemBottom - this.view.renderHeight);
1994
}
1995
}
1996
}
1997
1998
/**
1999
* Returns the relative position of an element rendered in the list.
2000
* Returns `null` if the element isn't *entirely* in the visible viewport.
2001
*/
2002
getRelativeTop(index: number, paddingTop: number = 0): number | null {
2003
if (index < 0 || index >= this.length) {
2004
throw new ListError(this.user, `Invalid index ${index}`);
2005
}
2006
2007
const scrollTop = this.view.getScrollTop();
2008
const elementTop = this.view.elementTop(index);
2009
const elementHeight = this.view.elementHeight(index);
2010
2011
if (elementTop < scrollTop + paddingTop || elementTop + elementHeight > scrollTop + this.view.renderHeight) {
2012
return null;
2013
}
2014
2015
// y = mx + b
2016
const m = elementHeight - this.view.renderHeight + paddingTop;
2017
return Math.abs((scrollTop + paddingTop - elementTop) / m);
2018
}
2019
2020
isDOMFocused(): boolean {
2021
return isActiveElement(this.view.domNode);
2022
}
2023
2024
getHTMLElement(): HTMLElement {
2025
return this.view.domNode;
2026
}
2027
2028
getScrollableElement(): HTMLElement {
2029
return this.view.scrollableElementDomNode;
2030
}
2031
2032
getElementID(index: number): string {
2033
return this.view.getElementDomId(index);
2034
}
2035
2036
getElementTop(index: number): number {
2037
return this.view.elementTop(index);
2038
}
2039
2040
style(styles: IListStyles): void {
2041
this.styleController.style(styles);
2042
}
2043
2044
delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
2045
this.view.delegateScrollFromMouseWheelEvent(browserEvent);
2046
}
2047
2048
private toListEvent({ indexes, browserEvent }: ITraitChangeEvent) {
2049
return { indexes, elements: indexes.map(i => this.view.element(i)), browserEvent };
2050
}
2051
2052
private _onFocusChange(): void {
2053
const focus = this.focus.get();
2054
this.view.domNode.classList.toggle('element-focused', focus.length > 0);
2055
this.onDidChangeActiveDescendant();
2056
}
2057
2058
private onDidChangeActiveDescendant(): void {
2059
const focus = this.focus.get();
2060
2061
if (focus.length > 0) {
2062
let id: string | undefined;
2063
2064
if (this.accessibilityProvider?.getActiveDescendantId) {
2065
id = this.accessibilityProvider.getActiveDescendantId(this.view.element(focus[0]));
2066
}
2067
2068
this.view.domNode.setAttribute('aria-activedescendant', id || this.view.getElementDomId(focus[0]));
2069
} else {
2070
this.view.domNode.removeAttribute('aria-activedescendant');
2071
}
2072
}
2073
2074
private _onSelectionChange(): void {
2075
const selection = this.selection.get();
2076
2077
this.view.domNode.classList.toggle('selection-none', selection.length === 0);
2078
this.view.domNode.classList.toggle('selection-single', selection.length === 1);
2079
this.view.domNode.classList.toggle('selection-multiple', selection.length > 1);
2080
}
2081
2082
dispose(): void {
2083
this._onDidDispose.fire();
2084
this.disposables.dispose();
2085
2086
this._onDidDispose.dispose();
2087
}
2088
}
2089
2090