Path: blob/main/src/vs/base/browser/ui/list/listWidget.ts
5220 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IDragAndDropData } from '../../dnd.js';6import { Dimension, EventHelper, getActiveElement, getWindow, isActiveElement, isEditableElement, isHTMLElement, isMouseEvent } from '../../dom.js';7import { createStyleSheet } from '../../domStylesheets.js';8import { asCssValueWithDefault } from '../../cssValue.js';9import { DomEmitter } from '../../event.js';10import { IKeyboardEvent, StandardKeyboardEvent } from '../../keyboardEvent.js';11import { Gesture } from '../../touch.js';12import { alert, AriaRole } from '../aria/aria.js';13import { CombinedSpliceable } from './splice.js';14import { ScrollableElementChangeOptions } from '../scrollbar/scrollableElementOptions.js';15import { binarySearch, range } from '../../../common/arrays.js';16import { timeout } from '../../../common/async.js';17import { Color } from '../../../common/color.js';18import { memoize } from '../../../common/decorators.js';19import { Emitter, Event, EventBufferer } from '../../../common/event.js';20import { matchesFuzzy2, matchesPrefix } from '../../../common/filters.js';21import { KeyCode } from '../../../common/keyCodes.js';22import { DisposableStore, dispose, IDisposable } from '../../../common/lifecycle.js';23import { clamp } from '../../../common/numbers.js';24import * as platform from '../../../common/platform.js';25import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';26import { ISpliceable } from '../../../common/sequence.js';27import { isNumber } from '../../../common/types.js';28import './list.css';29import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListEvent, IListGestureEvent, IListMouseEvent, IListElementRenderDetails, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListError, NotSelectableGroupId, NotSelectableGroupIdType } from './list.js';30import { IListView, IListViewAccessibilityProvider, IListViewDragAndDrop, IListViewOptions, IListViewOptionsUpdate, ListViewTargetSector, ListView } from './listView.js';31import { IMouseWheelEvent, StandardMouseEvent } from '../../mouseEvent.js';32import { autorun, constObservable, IObservable } from '../../../common/observable.js';3334interface ITraitChangeEvent {35indexes: number[];36browserEvent?: UIEvent;37}3839type ITraitTemplateData = HTMLElement;4041type IAccessibilityTemplateData = {42container: HTMLElement;43disposables: DisposableStore;44};4546interface IRenderedContainer {47templateData: ITraitTemplateData;48index: number;49}5051class TraitRenderer<T> implements IListRenderer<T, ITraitTemplateData> {52private renderedElements: IRenderedContainer[] = [];5354constructor(private trait: Trait<T>) { }5556get templateId(): string {57return `template:${this.trait.name}`;58}5960renderTemplate(container: HTMLElement): ITraitTemplateData {61return container;62}6364renderElement(element: T, index: number, templateData: ITraitTemplateData): void {65const renderedElementIndex = this.renderedElements.findIndex(el => el.templateData === templateData);6667if (renderedElementIndex >= 0) {68const rendered = this.renderedElements[renderedElementIndex];69this.trait.unrender(templateData);70rendered.index = index;71} else {72const rendered = { index, templateData };73this.renderedElements.push(rendered);74}7576this.trait.renderIndex(index, templateData);77}7879splice(start: number, deleteCount: number, insertCount: number): void {80const rendered: IRenderedContainer[] = [];8182for (const renderedElement of this.renderedElements) {8384if (renderedElement.index < start) {85rendered.push(renderedElement);86} else if (renderedElement.index >= start + deleteCount) {87rendered.push({88index: renderedElement.index + insertCount - deleteCount,89templateData: renderedElement.templateData90});91}92}9394this.renderedElements = rendered;95}9697renderIndexes(indexes: number[]): void {98for (const { index, templateData } of this.renderedElements) {99if (indexes.indexOf(index) > -1) {100this.trait.renderIndex(index, templateData);101}102}103}104105disposeTemplate(templateData: ITraitTemplateData): void {106const index = this.renderedElements.findIndex(el => el.templateData === templateData);107108if (index < 0) {109return;110}111112this.renderedElements.splice(index, 1);113}114}115116class Trait<T> implements ISpliceable<boolean>, IDisposable {117118protected indexes: number[] = [];119protected sortedIndexes: number[] = [];120121private readonly _onChange = new Emitter<ITraitChangeEvent>();122get onChange(): Event<ITraitChangeEvent> { return this._onChange.event; }123124get name(): string { return this._trait; }125126@memoize127get renderer(): TraitRenderer<T> {128return new TraitRenderer<T>(this);129}130131constructor(private _trait: string) { }132133splice(start: number, deleteCount: number, elements: boolean[]): void {134const diff = elements.length - deleteCount;135const end = start + deleteCount;136const sortedIndexes: number[] = [];137let i = 0;138139while (i < this.sortedIndexes.length && this.sortedIndexes[i] < start) {140sortedIndexes.push(this.sortedIndexes[i++]);141}142143for (let j = 0; j < elements.length; j++) {144if (elements[j]) {145sortedIndexes.push(j + start);146}147}148149while (i < this.sortedIndexes.length && this.sortedIndexes[i] >= end) {150sortedIndexes.push(this.sortedIndexes[i++] + diff);151}152153this.renderer.splice(start, deleteCount, elements.length);154this._set(sortedIndexes, sortedIndexes);155}156157renderIndex(index: number, container: HTMLElement): void {158container.classList.toggle(this._trait, this.contains(index));159}160161unrender(container: HTMLElement): void {162container.classList.remove(this._trait);163}164165/**166* Sets the indexes which should have this trait.167*168* @param indexes Indexes which should have this trait.169* @return The old indexes which had this trait.170*/171set(indexes: number[], browserEvent?: UIEvent): number[] {172return this._set(indexes, [...indexes].sort(numericSort), browserEvent);173}174175private _set(indexes: number[], sortedIndexes: number[], browserEvent?: UIEvent): number[] {176const result = this.indexes;177const sortedResult = this.sortedIndexes;178179this.indexes = indexes;180this.sortedIndexes = sortedIndexes;181182const toRender = disjunction(sortedResult, indexes);183this.renderer.renderIndexes(toRender);184185this._onChange.fire({ indexes, browserEvent });186return result;187}188189get(): number[] {190return this.indexes;191}192193contains(index: number): boolean {194return binarySearch(this.sortedIndexes, index, numericSort) >= 0;195}196197dispose() {198dispose(this._onChange);199}200}201202class SelectionTrait<T> extends Trait<T> {203204constructor(private setAriaSelected: boolean) {205super('selected');206}207208override renderIndex(index: number, container: HTMLElement): void {209super.renderIndex(index, container);210211if (this.setAriaSelected) {212if (this.contains(index)) {213container.setAttribute('aria-selected', 'true');214} else {215container.setAttribute('aria-selected', 'false');216}217}218}219}220221/**222* The TraitSpliceable is used as a util class to be able223* to preserve traits across splice calls, given an identity224* provider.225*/226class TraitSpliceable<T> implements ISpliceable<T> {227228constructor(229private trait: Trait<T>,230private view: IListView<T>,231private identityProvider?: IIdentityProvider<T>232) { }233234splice(start: number, deleteCount: number, elements: T[]): void {235if (!this.identityProvider) {236return this.trait.splice(start, deleteCount, new Array(elements.length).fill(false));237}238239const pastElementsWithTrait = this.trait.get().map(i => this.identityProvider!.getId(this.view.element(i)).toString());240if (pastElementsWithTrait.length === 0) {241return this.trait.splice(start, deleteCount, new Array(elements.length).fill(false));242}243244const pastElementsWithTraitSet = new Set(pastElementsWithTrait);245const elementsWithTrait = elements.map(e => pastElementsWithTraitSet.has(this.identityProvider!.getId(e).toString()));246this.trait.splice(start, deleteCount, elementsWithTrait);247}248}249250function isListElementDescendantOfClass(e: HTMLElement, className: string): boolean {251if (e.classList.contains(className)) {252return true;253}254255if (e.classList.contains('monaco-list')) {256return false;257}258259if (!e.parentElement) {260return false;261}262263return isListElementDescendantOfClass(e.parentElement, className);264}265266export function isMonacoEditor(e: HTMLElement): boolean {267return isListElementDescendantOfClass(e, 'monaco-editor');268}269270export function isMonacoCustomToggle(e: HTMLElement): boolean {271return isListElementDescendantOfClass(e, 'monaco-custom-toggle');272}273274export function isActionItem(e: HTMLElement): boolean {275return isListElementDescendantOfClass(e, 'action-item');276}277278export function isMonacoTwistie(e: HTMLElement): boolean {279return isListElementDescendantOfClass(e, 'monaco-tl-twistie');280}281282export function isStickyScrollElement(e: HTMLElement): boolean {283return isListElementDescendantOfClass(e, 'monaco-tree-sticky-row');284}285286export function isStickyScrollContainer(e: HTMLElement): boolean {287return e.classList.contains('monaco-tree-sticky-container');288}289290export function isButton(e: HTMLElement): boolean {291if ((e.tagName === 'A' && e.classList.contains('monaco-button')) ||292(e.tagName === 'DIV' && e.classList.contains('monaco-button-dropdown'))) {293return true;294}295296if (e.classList.contains('monaco-list')) {297return false;298}299300if (!e.parentElement) {301return false;302}303304return isButton(e.parentElement);305}306307class KeyboardController<T> implements IDisposable {308309private readonly disposables = new DisposableStore();310private readonly multipleSelectionDisposables = new DisposableStore();311private multipleSelectionSupport: boolean | undefined;312313@memoize314private get onKeyDown(): Event<StandardKeyboardEvent> {315return Event.chain(316this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>317$.filter(e => !isEditableElement(e.target as HTMLElement))318.map(e => new StandardKeyboardEvent(e))319);320}321322constructor(323private list: List<T>,324private view: IListView<T>,325options: IListOptions<T>326) {327this.multipleSelectionSupport = options.multipleSelectionSupport;328this.disposables.add(this.onKeyDown(e => {329switch (e.keyCode) {330case KeyCode.Enter:331return this.onEnter(e);332case KeyCode.UpArrow:333return this.onUpArrow(e);334case KeyCode.DownArrow:335return this.onDownArrow(e);336case KeyCode.PageUp:337return this.onPageUpArrow(e);338case KeyCode.PageDown:339return this.onPageDownArrow(e);340case KeyCode.Escape:341return this.onEscape(e);342case KeyCode.KeyA:343if (this.multipleSelectionSupport && (platform.isMacintosh ? e.metaKey : e.ctrlKey)) {344this.onCtrlA(e);345}346}347}));348}349350updateOptions(optionsUpdate: IListOptionsUpdate): void {351if (optionsUpdate.multipleSelectionSupport !== undefined) {352this.multipleSelectionSupport = optionsUpdate.multipleSelectionSupport;353}354}355356private onEnter(e: StandardKeyboardEvent): void {357e.preventDefault();358e.stopPropagation();359this.list.setSelection(this.list.getFocus(), e.browserEvent);360}361362private onUpArrow(e: StandardKeyboardEvent): void {363e.preventDefault();364e.stopPropagation();365this.list.focusPrevious(1, false, e.browserEvent);366const el = this.list.getFocus()[0];367this.list.setAnchor(el);368this.list.reveal(el);369this.view.domNode.focus();370}371372private onDownArrow(e: StandardKeyboardEvent): void {373e.preventDefault();374e.stopPropagation();375this.list.focusNext(1, false, e.browserEvent);376const el = this.list.getFocus()[0];377this.list.setAnchor(el);378this.list.reveal(el);379this.view.domNode.focus();380}381382private onPageUpArrow(e: StandardKeyboardEvent): void {383e.preventDefault();384e.stopPropagation();385this.list.focusPreviousPage(e.browserEvent);386const el = this.list.getFocus()[0];387this.list.setAnchor(el);388this.list.reveal(el);389this.view.domNode.focus();390}391392private onPageDownArrow(e: StandardKeyboardEvent): void {393e.preventDefault();394e.stopPropagation();395this.list.focusNextPage(e.browserEvent);396const el = this.list.getFocus()[0];397this.list.setAnchor(el);398this.list.reveal(el);399this.view.domNode.focus();400}401402private onCtrlA(e: StandardKeyboardEvent): void {403e.preventDefault();404e.stopPropagation();405406let selection = range(this.list.length);407408// Filter by group if identity provider has getGroupId409const focusedElements = this.list.getFocus();410const referenceGroupId = focusedElements.length > 0 ? this.list.getElementGroupId(focusedElements[0]) : undefined;411if (referenceGroupId !== undefined) {412selection = this.list.filterIndicesByGroup(selection, referenceGroupId);413}414415this.list.setSelection(selection, e.browserEvent);416this.list.setAnchor(undefined);417this.view.domNode.focus();418}419420private onEscape(e: StandardKeyboardEvent): void {421if (this.list.getSelection().length) {422e.preventDefault();423e.stopPropagation();424this.list.setSelection([], e.browserEvent);425this.list.setAnchor(undefined);426this.view.domNode.focus();427}428}429430dispose() {431this.disposables.dispose();432this.multipleSelectionDisposables.dispose();433}434}435436export enum TypeNavigationMode {437Automatic,438Trigger439}440441enum TypeNavigationControllerState {442Idle,443Typing444}445446export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardNavigationDelegate {447mightProducePrintableCharacter(event: IKeyboardEvent): boolean {448if (event.ctrlKey || event.metaKey || event.altKey) {449return false;450}451452return (event.keyCode >= KeyCode.KeyA && event.keyCode <= KeyCode.KeyZ)453|| (event.keyCode >= KeyCode.Digit0 && event.keyCode <= KeyCode.Digit9)454|| (event.keyCode >= KeyCode.Numpad0 && event.keyCode <= KeyCode.Numpad9)455|| (event.keyCode >= KeyCode.Semicolon && event.keyCode <= KeyCode.Quote);456}457};458459class TypeNavigationController<T> implements IDisposable {460461private enabled = false;462private state: TypeNavigationControllerState = TypeNavigationControllerState.Idle;463464private mode = TypeNavigationMode.Automatic;465private triggered = false;466private previouslyFocused = -1;467468private readonly enabledDisposables = new DisposableStore();469private readonly disposables = new DisposableStore();470471constructor(472private list: List<T>,473private view: IListView<T>,474private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>,475private keyboardNavigationEventFilter: IKeyboardNavigationEventFilter,476private delegate: IKeyboardNavigationDelegate477) {478this.updateOptions(list.options);479}480481updateOptions(options: IListOptions<T>): void {482if (options.typeNavigationEnabled ?? true) {483this.enable();484} else {485this.disable();486}487488this.mode = options.typeNavigationMode ?? TypeNavigationMode.Automatic;489}490491trigger(): void {492this.triggered = !this.triggered;493}494495private enable(): void {496if (this.enabled) {497return;498}499500let typing = false;501502const onChar = Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>503$.filter(e => !isEditableElement(e.target as HTMLElement))504.filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered)505.map(event => new StandardKeyboardEvent(event))506.filter(e => typing || this.keyboardNavigationEventFilter(e))507.filter(e => this.delegate.mightProducePrintableCharacter(e))508.forEach(e => EventHelper.stop(e, true))509.map(event => event.browserEvent.key)510);511512const onClear = Event.debounce<string, null>(onChar, () => null, 800, undefined, undefined, undefined, this.enabledDisposables);513const onInput = Event.reduce<string | null, string | null>(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i), undefined, this.enabledDisposables);514515onInput(this.onInput, this, this.enabledDisposables);516onClear(this.onClear, this, this.enabledDisposables);517518onChar(() => typing = true, undefined, this.enabledDisposables);519onClear(() => typing = false, undefined, this.enabledDisposables);520521this.enabled = true;522this.triggered = false;523}524525private disable(): void {526if (!this.enabled) {527return;528}529530this.enabledDisposables.clear();531this.enabled = false;532this.triggered = false;533}534535private onClear(): void {536const focus = this.list.getFocus();537if (focus.length > 0 && focus[0] === this.previouslyFocused) {538// List: re-announce element on typing end since typed keys will interrupt aria label of focused element539// Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961540const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0]));541542if (typeof ariaLabel === 'string') {543alert(ariaLabel);544} else if (ariaLabel) {545alert(ariaLabel.get());546}547}548this.previouslyFocused = -1;549}550551private onInput(word: string | null): void {552if (!word) {553this.state = TypeNavigationControllerState.Idle;554this.triggered = false;555return;556}557558const focus = this.list.getFocus();559const start = focus.length > 0 ? focus[0] : 0;560const delta = this.state === TypeNavigationControllerState.Idle ? 1 : 0;561this.state = TypeNavigationControllerState.Typing;562563for (let i = 0; i < this.list.length; i++) {564const index = (start + i + delta) % this.list.length;565const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(this.view.element(index));566const labelStr = label && label.toString();567568if (this.list.options.typeNavigationEnabled) {569if (typeof labelStr !== 'undefined') {570571// If prefix is found, focus and return early572if (matchesPrefix(word, labelStr)) {573this.previouslyFocused = start;574this.list.setFocus([index]);575this.list.reveal(index);576return;577}578579const fuzzy = matchesFuzzy2(word, labelStr);580581if (fuzzy) {582const fuzzyScore = fuzzy[0].end - fuzzy[0].start;583// 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.584if (fuzzyScore > 1 && fuzzy.length === 1) {585this.previouslyFocused = start;586this.list.setFocus([index]);587this.list.reveal(index);588return;589}590}591}592} else if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) {593this.previouslyFocused = start;594this.list.setFocus([index]);595this.list.reveal(index);596return;597}598}599}600601dispose() {602this.disable();603this.enabledDisposables.dispose();604this.disposables.dispose();605}606}607608class DOMFocusController<T> implements IDisposable {609610private readonly disposables = new DisposableStore();611612constructor(613private list: List<T>,614private view: IListView<T>615) {616const onKeyDown = Event.chain(this.disposables.add(new DomEmitter(view.domNode, 'keydown')).event, $ => $617.filter(e => !isEditableElement(e.target as HTMLElement))618.map(e => new StandardKeyboardEvent(e))619);620621const onTab = Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey));622623onTab(this.onTab, this, this.disposables);624}625626private onTab(e: StandardKeyboardEvent): void {627if (e.target !== this.view.domNode) {628return;629}630631const focus = this.list.getFocus();632633if (focus.length === 0) {634return;635}636637const focusedDomElement = this.view.domElement(focus[0]);638639if (!focusedDomElement) {640return;641}642643// eslint-disable-next-line no-restricted-syntax644const tabIndexElement = focusedDomElement.querySelector('[tabIndex]');645646if (!tabIndexElement || !(isHTMLElement(tabIndexElement)) || tabIndexElement.tabIndex === -1) {647return;648}649650const style = getWindow(tabIndexElement).getComputedStyle(tabIndexElement);651if (style.visibility === 'hidden' || style.display === 'none') {652return;653}654655e.preventDefault();656e.stopPropagation();657tabIndexElement.focus();658}659660dispose() {661this.disposables.dispose();662}663}664665export function isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {666return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;667}668669export function isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {670return event.browserEvent.shiftKey;671}672673function isMouseRightClick(event: UIEvent): boolean {674return isMouseEvent(event) && event.button === 2;675}676677const DefaultMultipleSelectionController = {678isSelectionSingleChangeEvent,679isSelectionRangeChangeEvent680};681682export class MouseController<T> implements IDisposable {683684private multipleSelectionController: IMultipleSelectionController<T> | undefined;685private readonly mouseSupport: boolean;686private readonly disposables = new DisposableStore();687688private readonly _onPointer = this.disposables.add(new Emitter<IListMouseEvent<T>>());689get onPointer() { return this._onPointer.event; }690691constructor(protected list: List<T>) {692if (list.options.multipleSelectionSupport !== false) {693this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController;694}695696this.mouseSupport = typeof list.options.mouseSupport === 'undefined' || !!list.options.mouseSupport;697698if (this.mouseSupport) {699list.onMouseDown(this.onMouseDown, this, this.disposables);700list.onContextMenu(this.onContextMenu, this, this.disposables);701list.onMouseDblClick(this.onDoubleClick, this, this.disposables);702list.onTouchStart(this.onMouseDown, this, this.disposables);703this.disposables.add(Gesture.addTarget(list.getHTMLElement()));704}705706Event.any<IListMouseEvent<any> | IListGestureEvent<any>>(list.onMouseClick, list.onMouseMiddleClick, list.onTap)(this.onViewPointer, this, this.disposables);707}708709updateOptions(optionsUpdate: IListOptionsUpdate): void {710if (optionsUpdate.multipleSelectionSupport !== undefined) {711this.multipleSelectionController = undefined;712713if (optionsUpdate.multipleSelectionSupport) {714this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController;715}716}717}718719protected isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {720if (!this.multipleSelectionController) {721return false;722}723724return this.multipleSelectionController.isSelectionSingleChangeEvent(event);725}726727protected isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {728if (!this.multipleSelectionController) {729return false;730}731732return this.multipleSelectionController.isSelectionRangeChangeEvent(event);733}734735private isSelectionChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {736return this.isSelectionSingleChangeEvent(event) || this.isSelectionRangeChangeEvent(event);737}738739protected onMouseDown(e: IListMouseEvent<T> | IListTouchEvent<T>): void {740if (isMonacoEditor(e.browserEvent.target as HTMLElement)) {741return;742}743744if (getActiveElement() !== e.browserEvent.target) {745this.list.domFocus();746}747}748749protected onContextMenu(e: IListContextMenuEvent<T>): void {750if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {751return;752}753754const focus = typeof e.index === 'undefined' ? [] : [e.index];755this.list.setFocus(focus, e.browserEvent);756}757758protected onViewPointer(e: IListMouseEvent<T>): void {759if (!this.mouseSupport) {760return;761}762763if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {764return;765}766767if (e.browserEvent.isHandledByList) {768return;769}770771e.browserEvent.isHandledByList = true;772const focus = e.index;773774if (typeof focus === 'undefined') {775this.list.setFocus([], e.browserEvent);776this.list.setSelection([], e.browserEvent);777this.list.setAnchor(undefined);778return;779}780781if (this.isSelectionChangeEvent(e)) {782return this.changeSelection(e);783}784785this.list.setFocus([focus], e.browserEvent);786this.list.setAnchor(focus);787788if (!isMouseRightClick(e.browserEvent)) {789// Check if the element is selectable (getGroupId must not return undefined)790const focusGroupId = this.list.getElementGroupId(focus);791if (focusGroupId !== NotSelectableGroupId) {792this.list.setSelection([focus], e.browserEvent);793}794}795796this._onPointer.fire(e);797}798799protected onDoubleClick(e: IListMouseEvent<T>): void {800if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {801return;802}803804if (this.isSelectionChangeEvent(e)) {805return;806}807808if (e.browserEvent.isHandledByList) {809return;810}811812e.browserEvent.isHandledByList = true;813const focus = this.list.getFocus();814this.list.setSelection(focus, e.browserEvent);815}816817private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>): void {818const focus = e.index!;819let anchor = this.list.getAnchor();820821if (this.isSelectionRangeChangeEvent(e)) {822if (typeof anchor === 'undefined') {823const currentFocus = this.list.getFocus()[0];824anchor = currentFocus ?? focus;825this.list.setAnchor(anchor);826}827828const min = Math.min(anchor, focus);829const max = Math.max(anchor, focus);830let rangeSelection = range(min, max + 1);831832const selectedElement = this.list.getSelection()[0];833if (selectedElement !== undefined) {834const referenceGroupId = this.list.getElementGroupId(selectedElement);835if (referenceGroupId !== undefined) {836rangeSelection = this.list.filterIndicesByGroup(rangeSelection, referenceGroupId);837}838}839840const selection = this.list.getSelection();841const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor);842843if (contiguousRange.length === 0) {844return;845}846847const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange));848this.list.setSelection(newSelection, e.browserEvent);849this.list.setFocus([focus], e.browserEvent);850851} else if (this.isSelectionSingleChangeEvent(e)) {852const selection = this.list.getSelection();853const newSelection = selection.filter(i => i !== focus);854855this.list.setFocus([focus]);856this.list.setAnchor(focus);857858const focusGroupId = this.list.getElementGroupId(focus);859if (focusGroupId === NotSelectableGroupId) {860return; // Cannot select this element, do nothing861}862863if (selection.length === newSelection.length) {864const itemsToBeSelected = focusGroupId !== undefined ?865this.list.filterIndicesByGroup([...newSelection, focus], focusGroupId)866: [...newSelection, focus];867this.list.setSelection(itemsToBeSelected, e.browserEvent);868} else {869this.list.setSelection(newSelection, e.browserEvent);870}871}872}873874dispose() {875this.disposables.dispose();876}877}878879export interface IMultipleSelectionController<T> {880isSelectionSingleChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;881isSelectionRangeChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;882}883884export interface IStyleController {885style(styles: IListStyles): void;886}887888export interface IListAccessibilityProvider<T> extends IListViewAccessibilityProvider<T> {889getAriaLabel(element: T): string | IObservable<string> | null;890getWidgetAriaLabel(): string | IObservable<string>;891getWidgetRole?(): AriaRole;892getAriaLevel?(element: T): number | undefined;893readonly onDidChangeActiveDescendant?: Event<void>;894getActiveDescendantId?(element: T): string | undefined;895}896897export class DefaultStyleController implements IStyleController {898899constructor(private styleElement: HTMLStyleElement, private selectorSuffix: string) { }900901style(styles: IListStyles): void {902const suffix = this.selectorSuffix && `.${this.selectorSuffix}`;903const content: string[] = [];904905if (styles.listBackground) {906content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`);907}908909if (styles.listFocusBackground) {910content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`);911content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case!912}913914if (styles.listFocusForeground) {915content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`);916}917918if (styles.listActiveSelectionBackground) {919content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`);920content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case!921}922923if (styles.listActiveSelectionForeground) {924content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`);925}926927if (styles.listActiveSelectionIconForeground) {928content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; }`);929}930931if (styles.listFocusAndSelectionBackground) {932content.push(`933.monaco-drag-image${suffix},934.monaco-list${suffix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }935`);936}937938if (styles.listFocusAndSelectionForeground) {939content.push(`940.monaco-drag-image${suffix},941.monaco-list${suffix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }942`);943}944945if (styles.listInactiveFocusForeground) {946content.push(`.monaco-list${suffix} .monaco-list-row.focused { color: ${styles.listInactiveFocusForeground}; }`);947content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { color: ${styles.listInactiveFocusForeground}; }`); // overwrite :hover style in this case!948}949950if (styles.listInactiveSelectionIconForeground) {951content.push(`.monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; }`);952}953954if (styles.listInactiveFocusBackground) {955content.push(`.monaco-list${suffix} .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`);956content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!957}958959if (styles.listInactiveSelectionBackground) {960content.push(`.monaco-list${suffix} .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`);961content.push(`.monaco-list${suffix} .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case!962}963964if (styles.listInactiveSelectionForeground) {965content.push(`.monaco-list${suffix} .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`);966}967968if (styles.listHoverBackground) {969content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);970}971972if (styles.listHoverForeground) {973content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`);974}975976/**977* Outlines978*/979const focusAndSelectionOutline = asCssValueWithDefault(styles.listFocusAndSelectionOutline, asCssValueWithDefault(styles.listSelectionOutline, styles.listFocusOutline ?? ''));980if (focusAndSelectionOutline) { // default: listFocusOutline981content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused.selected { outline: 1px solid ${focusAndSelectionOutline}; outline-offset: -1px;}`);982}983984if (styles.listFocusOutline) { // default: set985content.push(`986.monaco-drag-image${suffix},987.monaco-list${suffix}:focus .monaco-list-row.focused,988.context-menu-visible .monaco-list${suffix}.last-focused .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }989`);990}991992const inactiveFocusAndSelectionOutline = asCssValueWithDefault(styles.listSelectionOutline, styles.listInactiveFocusOutline ?? '');993if (inactiveFocusAndSelectionOutline) {994content.push(`.monaco-list${suffix} .monaco-list-row.focused.selected { outline: 1px dotted ${inactiveFocusAndSelectionOutline}; outline-offset: -1px; }`);995}996997if (styles.listSelectionOutline) { // default: activeContrastBorder998content.push(`.monaco-list${suffix} .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`);999}10001001if (styles.listInactiveFocusOutline) { // default: null1002content.push(`.monaco-list${suffix} .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`);1003}10041005if (styles.listHoverOutline) { // default: activeContrastBorder1006content.push(`.monaco-list${suffix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);1007}10081009if (styles.listDropOverBackground) {1010content.push(`1011.monaco-list${suffix}.drop-target,1012.monaco-list${suffix} .monaco-list-rows.drop-target,1013.monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropOverBackground} !important; color: inherit !important; }1014`);1015}10161017if (styles.listDropBetweenBackground) {1018content.push(`1019.monaco-list${suffix} .monaco-list-rows.drop-target-before .monaco-list-row:first-child::before,1020.monaco-list${suffix} .monaco-list-row.drop-target-before::before {1021content: ""; position: absolute; top: 0px; left: 0px; width: 100%; height: 1px;1022background-color: ${styles.listDropBetweenBackground};1023}`);1024content.push(`1025.monaco-list${suffix} .monaco-list-rows.drop-target-after .monaco-list-row:last-child::after,1026.monaco-list${suffix} .monaco-list-row.drop-target-after::after {1027content: ""; position: absolute; bottom: 0px; left: 0px; width: 100%; height: 1px;1028background-color: ${styles.listDropBetweenBackground};1029}`);1030}10311032if (styles.tableColumnsBorder) {1033content.push(`1034.monaco-table > .monaco-split-view2,1035.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before,1036.monaco-enable-motion .monaco-table:hover > .monaco-split-view2,1037.monaco-enable-motion .monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before {1038border-color: ${styles.tableColumnsBorder};1039}10401041.monaco-enable-motion .monaco-table > .monaco-split-view2,1042.monaco-enable-motion .monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {1043border-color: transparent;1044}1045`);1046}10471048if (styles.tableOddRowsBackgroundColor) {1049content.push(`1050.monaco-table .monaco-list-row[data-parity=odd]:not(.focused):not(.selected):not(:hover) .monaco-table-tr,1051.monaco-table .monaco-list:not(:focus) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr,1052.monaco-table .monaco-list:not(.focused) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr {1053background-color: ${styles.tableOddRowsBackgroundColor};1054}1055`);1056}10571058this.styleElement.textContent = content.join('\n');1059}1060}10611062export interface IKeyboardNavigationEventFilter {1063(e: StandardKeyboardEvent): boolean;1064}10651066export interface IListOptionsUpdate extends IListViewOptionsUpdate {1067readonly typeNavigationEnabled?: boolean;1068readonly typeNavigationMode?: TypeNavigationMode;1069readonly multipleSelectionSupport?: boolean;1070}10711072export interface IListOptions<T> extends IListOptionsUpdate {1073readonly identityProvider?: IIdentityProvider<T>;1074readonly dnd?: IListDragAndDrop<T>;1075readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider<T>;1076readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate;1077readonly keyboardSupport?: boolean;1078readonly multipleSelectionController?: IMultipleSelectionController<T>;1079readonly styleController?: (suffix: string) => IStyleController;1080readonly accessibilityProvider?: IListAccessibilityProvider<T>;1081readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;10821083// list view options1084readonly useShadows?: boolean;1085readonly verticalScrollMode?: ScrollbarVisibility;1086readonly setRowLineHeight?: boolean;1087readonly setRowHeight?: boolean;1088readonly supportDynamicHeights?: boolean;1089readonly mouseSupport?: boolean;1090readonly userSelection?: boolean;1091readonly horizontalScrolling?: boolean;1092readonly scrollByPage?: boolean;1093readonly transformOptimization?: boolean;1094readonly smoothScrolling?: boolean;1095readonly scrollableElementChangeOptions?: ScrollableElementChangeOptions;1096readonly alwaysConsumeMouseWheel?: boolean;1097readonly initialSize?: Dimension;1098readonly paddingTop?: number;1099readonly paddingBottom?: number;1100}11011102export interface IListStyles {1103listBackground: string | undefined;1104listFocusBackground: string | undefined;1105listFocusForeground: string | undefined;1106listActiveSelectionBackground: string | undefined;1107listActiveSelectionForeground: string | undefined;1108listActiveSelectionIconForeground: string | undefined;1109listFocusAndSelectionOutline: string | undefined;1110listFocusAndSelectionBackground: string | undefined;1111listFocusAndSelectionForeground: string | undefined;1112listInactiveSelectionBackground: string | undefined;1113listInactiveSelectionIconForeground: string | undefined;1114listInactiveSelectionForeground: string | undefined;1115listInactiveFocusForeground: string | undefined;1116listInactiveFocusBackground: string | undefined;1117listHoverBackground: string | undefined;1118listHoverForeground: string | undefined;1119listDropOverBackground: string | undefined;1120listDropBetweenBackground: string | undefined;1121listFocusOutline: string | undefined;1122listInactiveFocusOutline: string | undefined;1123listSelectionOutline: string | undefined;1124listHoverOutline: string | undefined;1125treeIndentGuidesStroke: string | undefined;1126treeInactiveIndentGuidesStroke: string | undefined;1127treeStickyScrollBackground: string | undefined;1128treeStickyScrollBorder: string | undefined;1129treeStickyScrollShadow: string | undefined;1130tableColumnsBorder: string | undefined;1131tableOddRowsBackgroundColor: string | undefined;1132}11331134export const unthemedListStyles: IListStyles = {1135listFocusBackground: '#7FB0D0',1136listActiveSelectionBackground: '#0E639C',1137listActiveSelectionForeground: '#FFFFFF',1138listActiveSelectionIconForeground: '#FFFFFF',1139listFocusAndSelectionOutline: '#90C2F9',1140listFocusAndSelectionBackground: '#094771',1141listFocusAndSelectionForeground: '#FFFFFF',1142listInactiveSelectionBackground: '#3F3F46',1143listInactiveSelectionIconForeground: '#FFFFFF',1144listHoverBackground: '#2A2D2E',1145listDropOverBackground: '#383B3D',1146listDropBetweenBackground: '#EEEEEE',1147treeIndentGuidesStroke: '#a9a9a9',1148treeInactiveIndentGuidesStroke: Color.fromHex('#a9a9a9').transparent(0.4).toString(),1149tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2).toString(),1150tableOddRowsBackgroundColor: Color.fromHex('#cccccc').transparent(0.04).toString(),1151listBackground: undefined,1152listFocusForeground: undefined,1153listInactiveSelectionForeground: undefined,1154listInactiveFocusForeground: undefined,1155listInactiveFocusBackground: undefined,1156listHoverForeground: undefined,1157listFocusOutline: undefined,1158listInactiveFocusOutline: undefined,1159listSelectionOutline: undefined,1160listHoverOutline: undefined,1161treeStickyScrollBackground: undefined,1162treeStickyScrollBorder: undefined,1163treeStickyScrollShadow: undefined1164};11651166const DefaultOptions: IListOptions<any> = {1167keyboardSupport: true,1168mouseSupport: true,1169multipleSelectionSupport: true,1170dnd: {1171getDragURI() { return null; },1172onDragStart(): void { },1173onDragOver() { return false; },1174drop() { },1175dispose() { }1176}1177};11781179// TODO@Joao: move these utils into a SortedArray class11801181function getContiguousRangeContaining(range: number[], value: number): number[] {1182const index = range.indexOf(value);11831184if (index === -1) {1185return [];1186}11871188const result: number[] = [];1189let i = index - 1;1190while (i >= 0 && range[i] === value - (index - i)) {1191result.push(range[i--]);1192}11931194result.reverse();1195i = index;1196while (i < range.length && range[i] === value + (i - index)) {1197result.push(range[i++]);1198}11991200return result;1201}12021203/**1204* Given two sorted collections of numbers, returns the intersection1205* between them (OR).1206*/1207function disjunction(one: number[], other: number[]): number[] {1208const result: number[] = [];1209let i = 0, j = 0;12101211while (i < one.length || j < other.length) {1212if (i >= one.length) {1213result.push(other[j++]);1214} else if (j >= other.length) {1215result.push(one[i++]);1216} else if (one[i] === other[j]) {1217result.push(one[i]);1218i++;1219j++;1220continue;1221} else if (one[i] < other[j]) {1222result.push(one[i++]);1223} else {1224result.push(other[j++]);1225}1226}12271228return result;1229}12301231/**1232* Given two sorted collections of numbers, returns the relative1233* complement between them (XOR).1234*/1235function relativeComplement(one: number[], other: number[]): number[] {1236const result: number[] = [];1237let i = 0, j = 0;12381239while (i < one.length || j < other.length) {1240if (i >= one.length) {1241result.push(other[j++]);1242} else if (j >= other.length) {1243result.push(one[i++]);1244} else if (one[i] === other[j]) {1245i++;1246j++;1247continue;1248} else if (one[i] < other[j]) {1249result.push(one[i++]);1250} else {1251j++;1252}1253}12541255return result;1256}12571258const numericSort = (a: number, b: number) => a - b;12591260class PipelineRenderer<T> implements IListRenderer<T, any> {12611262constructor(1263private _templateId: string,1264private renderers: IListRenderer<any /* TODO@joao */, any>[]1265) { }12661267get templateId(): string {1268return this._templateId;1269}12701271renderTemplate(container: HTMLElement): any[] {1272return this.renderers.map(r => r.renderTemplate(container));1273}12741275renderElement(element: T, index: number, templateData: any[], renderDetails?: IListElementRenderDetails): void {1276let i = 0;12771278for (const renderer of this.renderers) {1279renderer.renderElement(element, index, templateData[i++], renderDetails);1280}1281}12821283disposeElement(element: T, index: number, templateData: any[], renderDetails?: IListElementRenderDetails): void {1284let i = 0;12851286for (const renderer of this.renderers) {1287renderer.disposeElement?.(element, index, templateData[i], renderDetails);12881289i += 1;1290}1291}12921293disposeTemplate(templateData: unknown[]): void {1294let i = 0;12951296for (const renderer of this.renderers) {1297renderer.disposeTemplate(templateData[i++]);1298}1299}1300}13011302class AccessibiltyRenderer<T> implements IListRenderer<T, IAccessibilityTemplateData> {13031304templateId: string = 'a18n';13051306constructor(private accessibilityProvider: IListAccessibilityProvider<T>) { }13071308renderTemplate(container: HTMLElement): IAccessibilityTemplateData {1309return { container, disposables: new DisposableStore() };1310}13111312renderElement(element: T, index: number, data: IAccessibilityTemplateData): void {1313const ariaLabel = this.accessibilityProvider.getAriaLabel(element);1314const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel);13151316data.disposables.add(autorun(reader => {1317this.setAriaLabel(reader.readObservable(observable), data.container);1318}));13191320const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element);13211322if (typeof ariaLevel === 'number') {1323data.container.setAttribute('aria-level', `${ariaLevel}`);1324} else {1325data.container.removeAttribute('aria-level');1326}1327}13281329private setAriaLabel(ariaLabel: string | null, element: HTMLElement): void {1330if (ariaLabel) {1331element.setAttribute('aria-label', ariaLabel);1332} else {1333element.removeAttribute('aria-label');1334}1335}13361337disposeElement(element: T, index: number, templateData: IAccessibilityTemplateData): void {1338templateData.disposables.clear();1339}13401341disposeTemplate(templateData: IAccessibilityTemplateData): void {1342templateData.disposables.dispose();1343}1344}13451346class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {13471348constructor(private list: List<T>, private dnd: IListDragAndDrop<T>) { }13491350getDragElements(element: T): T[] {1351const selection = this.list.getSelectedElements();1352const elements = selection.indexOf(element) > -1 ? selection : [element];1353return elements;1354}13551356getDragURI(element: T): string | null {1357return this.dnd.getDragURI(element);1358}13591360getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined {1361if (this.dnd.getDragLabel) {1362return this.dnd.getDragLabel(elements, originalEvent);1363}13641365return undefined;1366}13671368onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {1369this.dnd.onDragStart?.(data, originalEvent);1370}13711372onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction {1373return this.dnd.onDragOver(data, targetElement, targetIndex, targetSector, originalEvent);1374}13751376onDragLeave(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {1377this.dnd.onDragLeave?.(data, targetElement, targetIndex, originalEvent);1378}13791380onDragEnd(originalEvent: DragEvent): void {1381this.dnd.onDragEnd?.(originalEvent);1382}13831384drop(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void {1385this.dnd.drop(data, targetElement, targetIndex, targetSector, originalEvent);1386}13871388dispose(): void {1389this.dnd.dispose();1390}1391}13921393/**1394* The {@link List} is a virtual scrolling widget, built on top of the {@link ListView}1395* widget.1396*1397* Features:1398* - Customizable keyboard and mouse support1399* - Element traits: focus, selection, achor1400* - Accessibility support1401* - Touch support1402* - Performant template-based rendering1403* - Horizontal scrolling1404* - Variable element height support1405* - Dynamic element height support1406* - Drag-and-drop support1407*/1408export class List<T> implements ISpliceable<T>, IDisposable {14091410private focus = new Trait<T>('focused');1411private selection: Trait<T>;1412private anchor = new Trait<T>('anchor');1413private eventBufferer = new EventBufferer();1414protected view: IListView<T>;1415private spliceable: ISpliceable<T>;1416private styleController: IStyleController;1417private typeNavigationController?: TypeNavigationController<T>;1418private accessibilityProvider?: IListAccessibilityProvider<T>;1419private keyboardController: KeyboardController<T> | undefined;1420private mouseController: MouseController<T>;1421private _ariaLabel: string = '';14221423protected readonly disposables = new DisposableStore();14241425@memoize get onDidChangeFocus(): Event<IListEvent<T>> {1426return Event.map(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toListEvent(e), this.disposables);1427}14281429@memoize get onDidChangeSelection(): Event<IListEvent<T>> {1430return Event.map(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toListEvent(e), this.disposables);1431}14321433get domId(): string { return this.view.domId; }1434get onDidScroll(): Event<ScrollEvent> { return this.view.onDidScroll; }1435get onMouseClick(): Event<IListMouseEvent<T>> { return this.view.onMouseClick; }1436get onMouseDblClick(): Event<IListMouseEvent<T>> { return this.view.onMouseDblClick; }1437get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return this.view.onMouseMiddleClick; }1438get onPointer(): Event<IListMouseEvent<T>> { return this.mouseController.onPointer; }1439get onMouseUp(): Event<IListMouseEvent<T>> { return this.view.onMouseUp; }1440get onMouseDown(): Event<IListMouseEvent<T>> { return this.view.onMouseDown; }1441get onMouseOver(): Event<IListMouseEvent<T>> { return this.view.onMouseOver; }1442get onMouseMove(): Event<IListMouseEvent<T>> { return this.view.onMouseMove; }1443get onMouseOut(): Event<IListMouseEvent<T>> { return this.view.onMouseOut; }1444get onTouchStart(): Event<IListTouchEvent<T>> { return this.view.onTouchStart; }1445get onTap(): Event<IListGestureEvent<T>> { return this.view.onTap; }14461447/**1448* Possible context menu trigger events:1449* - ContextMenu key1450* - Shift F101451* - Ctrl Option Shift M (macOS with VoiceOver)1452* - Mouse right click1453*/1454@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {1455let didJustPressContextMenuKey = false;14561457const fromKeyDown: Event<any> = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>1458$.map(e => new StandardKeyboardEvent(e))1459.filter(e => didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))1460.map(e => EventHelper.stop(e, true))1461.filter(() => false));14621463const fromKeyUp = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event, $ =>1464$.forEach(() => didJustPressContextMenuKey = false)1465.map(e => new StandardKeyboardEvent(e))1466.filter(e => e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))1467.map(e => EventHelper.stop(e, true))1468.map(({ browserEvent }) => {1469const focus = this.getFocus();1470const index = focus.length ? focus[0] : undefined;1471const element = typeof index !== 'undefined' ? this.view.element(index) : undefined;1472const anchor = typeof index !== 'undefined' ? this.view.domElement(index) as HTMLElement : this.view.domNode;1473return { index, element, anchor, browserEvent };1474}));14751476const fromMouse = Event.chain(this.view.onContextMenu, $ =>1477$.filter(_ => !didJustPressContextMenuKey)1478.map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(getWindow(this.view.domNode), browserEvent), browserEvent }))1479);14801481return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);1482}14831484@memoize get onKeyDown(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event; }1485@memoize get onKeyUp(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event; }1486@memoize get onKeyPress(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keypress')).event; }14871488@memoize get onDidFocus(): Event<void> { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'focus', true)).event); }1489@memoize get onDidBlur(): Event<void> { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'blur', true)).event); }14901491private readonly _onDidDispose = new Emitter<void>();1492readonly onDidDispose: Event<void> = this._onDidDispose.event;14931494constructor(1495private user: string,1496container: HTMLElement,1497virtualDelegate: IListVirtualDelegate<T>,1498renderers: IListRenderer<any /* TODO@joao */, any>[],1499private _options: IListOptions<T> = DefaultOptions1500) {1501const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list';1502this.selection = new SelectionTrait(role !== 'listbox');15031504const baseRenderers: IListRenderer<T, unknown>[] = [this.focus.renderer, this.selection.renderer];15051506this.accessibilityProvider = _options.accessibilityProvider;15071508if (this.accessibilityProvider) {1509baseRenderers.push(new AccessibiltyRenderer<T>(this.accessibilityProvider));15101511this.accessibilityProvider.onDidChangeActiveDescendant?.(this.onDidChangeActiveDescendant, this, this.disposables);1512}15131514renderers = renderers.map(r => new PipelineRenderer(r.templateId, [...baseRenderers, r]));15151516const viewOptions: IListViewOptions<T> = {1517..._options,1518dnd: _options.dnd && new ListViewDragAndDrop(this, _options.dnd)1519};15201521this.view = this.createListView(container, virtualDelegate, renderers, viewOptions);1522this.view.domNode.setAttribute('role', role);15231524if (_options.styleController) {1525this.styleController = _options.styleController(this.view.domId);1526} else {1527const styleElement = createStyleSheet(this.view.domNode);1528this.styleController = new DefaultStyleController(styleElement, this.view.domId);1529}15301531this.spliceable = new CombinedSpliceable([1532new TraitSpliceable(this.focus, this.view, _options.identityProvider),1533new TraitSpliceable(this.selection, this.view, _options.identityProvider),1534new TraitSpliceable(this.anchor, this.view, _options.identityProvider),1535this.view1536]);15371538this.disposables.add(this.focus);1539this.disposables.add(this.selection);1540this.disposables.add(this.anchor);1541this.disposables.add(this.view);1542this.disposables.add(this._onDidDispose);15431544this.disposables.add(new DOMFocusController(this, this.view));15451546if (typeof _options.keyboardSupport !== 'boolean' || _options.keyboardSupport) {1547this.keyboardController = new KeyboardController(this, this.view, _options);1548this.disposables.add(this.keyboardController);1549}15501551if (_options.keyboardNavigationLabelProvider) {1552const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;1553this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, _options.keyboardNavigationEventFilter ?? (() => true), delegate);1554this.disposables.add(this.typeNavigationController);1555}15561557this.mouseController = this.createMouseController(_options);1558this.disposables.add(this.mouseController);15591560this.onDidChangeFocus(this._onFocusChange, this, this.disposables);1561this.onDidChangeSelection(this._onSelectionChange, this, this.disposables);15621563if (this.accessibilityProvider) {1564const ariaLabel = this.accessibilityProvider.getWidgetAriaLabel();1565const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel);15661567this.disposables.add(autorun(reader => {1568this.ariaLabel = reader.readObservable(observable);1569}));1570}15711572if (this._options.multipleSelectionSupport !== false) {1573this.view.domNode.setAttribute('aria-multiselectable', 'true');1574}1575}15761577protected createListView(container: HTMLElement, virtualDelegate: IListVirtualDelegate<T>, renderers: IListRenderer<any, any>[], viewOptions: IListViewOptions<T>): IListView<T> {1578return new ListView(container, virtualDelegate, renderers, viewOptions);1579}15801581protected createMouseController(options: IListOptions<T>): MouseController<T> {1582return new MouseController(this);1583}15841585updateOptions(optionsUpdate: IListOptionsUpdate = {}): void {1586this._options = { ...this._options, ...optionsUpdate };15871588this.typeNavigationController?.updateOptions(this._options);15891590if (this._options.multipleSelectionController !== undefined) {1591if (this._options.multipleSelectionSupport) {1592this.view.domNode.setAttribute('aria-multiselectable', 'true');1593} else {1594this.view.domNode.removeAttribute('aria-multiselectable');1595}1596}15971598this.mouseController.updateOptions(optionsUpdate);1599this.keyboardController?.updateOptions(optionsUpdate);1600this.view.updateOptions(optionsUpdate);1601}16021603get options(): IListOptions<T> {1604return this._options;1605}16061607splice(start: number, deleteCount: number, elements: readonly T[] = []): void {1608if (start < 0 || start > this.view.length) {1609throw new ListError(this.user, `Invalid start index: ${start}`);1610}16111612if (deleteCount < 0) {1613throw new ListError(this.user, `Invalid delete count: ${deleteCount}`);1614}16151616if (deleteCount === 0 && elements.length === 0) {1617return;1618}16191620this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements));1621}16221623updateWidth(index: number): void {1624this.view.updateWidth(index);1625}16261627updateElementHeight(index: number, size: number | undefined): void {1628this.view.updateElementHeight(index, size, null);1629}16301631rerender(): void {1632this.view.rerender();1633}16341635element(index: number): T {1636return this.view.element(index);1637}16381639indexOf(element: T): number {1640return this.view.indexOf(element);1641}16421643indexAt(position: number): number {1644return this.view.indexAt(position);1645}16461647get length(): number {1648return this.view.length;1649}16501651get contentHeight(): number {1652return this.view.contentHeight;1653}16541655get contentWidth(): number {1656return this.view.contentWidth;1657}16581659get onDidChangeContentHeight(): Event<number> {1660return this.view.onDidChangeContentHeight;1661}16621663get onDidChangeContentWidth(): Event<number> {1664return this.view.onDidChangeContentWidth;1665}16661667get scrollTop(): number {1668return this.view.getScrollTop();1669}16701671set scrollTop(scrollTop: number) {1672this.view.setScrollTop(scrollTop);1673}16741675get scrollLeft(): number {1676return this.view.getScrollLeft();1677}16781679set scrollLeft(scrollLeft: number) {1680this.view.setScrollLeft(scrollLeft);1681}16821683get scrollHeight(): number {1684return this.view.scrollHeight;1685}16861687get renderHeight(): number {1688return this.view.renderHeight;1689}16901691get firstVisibleIndex(): number {1692return this.view.firstVisibleIndex;1693}16941695get firstMostlyVisibleIndex(): number {1696return this.view.firstMostlyVisibleIndex;1697}16981699get lastVisibleIndex(): number {1700return this.view.lastVisibleIndex;1701}17021703get ariaLabel(): string {1704return this._ariaLabel;1705}17061707set ariaLabel(value: string) {1708this._ariaLabel = value;1709this.view.domNode.setAttribute('aria-label', value);1710}17111712domFocus(): void {1713this.view.domNode.focus({ preventScroll: true });1714}17151716layout(height?: number, width?: number): void {1717this.view.layout(height, width);1718}17191720triggerTypeNavigation(): void {1721this.typeNavigationController?.trigger();1722}17231724setSelection(indexes: number[], browserEvent?: UIEvent): void {1725for (const index of indexes) {1726if (index < 0 || index >= this.length) {1727throw new ListError(this.user, `Invalid index ${index}`);1728}1729}17301731indexes = indexes.filter(i => this.getElementGroupId(i) !== NotSelectableGroupId);17321733this.selection.set(indexes, browserEvent);1734}17351736getSelection(): number[] {1737return this.selection.get();1738}17391740getSelectedElements(): T[] {1741return this.getSelection().map(i => this.view.element(i));1742}17431744setAnchor(index: number | undefined): void {1745if (typeof index === 'undefined') {1746this.anchor.set([]);1747return;1748}17491750if (index < 0 || index >= this.length) {1751throw new ListError(this.user, `Invalid index ${index}`);1752}17531754this.anchor.set([index]);1755}17561757getAnchor(): number | undefined {1758return this.anchor.get().at(0);1759}17601761getAnchorElement(): T | undefined {1762const anchor = this.getAnchor();1763return typeof anchor === 'undefined' ? undefined : this.element(anchor);1764}17651766/**1767* Gets the group ID for an element at the given index.1768* Returns undefined if no identity provider, no getGroupId method, or if the group ID is undefined.1769*/1770getElementGroupId(index: number): number | NotSelectableGroupIdType | undefined {1771const identityProvider = this.options.identityProvider;1772if (!identityProvider?.getGroupId) {1773return undefined;1774}17751776const element = this.element(index);1777return identityProvider.getGroupId(element);1778}17791780/**1781* Filters the given indices to only include those with a matching group ID.1782* If no identity provider or getGroupId method exists, returns the original indices.1783* If referenceGroupId is undefined, returns an empty array (elements without group IDs are not selectable).1784*/1785filterIndicesByGroup(indices: number[], referenceGroupId: number | NotSelectableGroupIdType): number[] {1786const identityProvider = this.options.identityProvider;1787if (!identityProvider?.getGroupId) {1788return indices;1789}17901791if (referenceGroupId === NotSelectableGroupId) {1792return [];1793}17941795return indices.filter(index => {1796const element = this.element(index);1797const groupId = identityProvider.getGroupId!(element);1798return groupId === referenceGroupId;1799});1800}18011802setFocus(indexes: number[], browserEvent?: UIEvent): void {1803for (const index of indexes) {1804if (index < 0 || index >= this.length) {1805throw new ListError(this.user, `Invalid index ${index}`);1806}1807}18081809this.focus.set(indexes, browserEvent);1810}18111812focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1813if (this.length === 0) { return; }18141815const focus = this.focus.get();1816const index = this.findNextIndex(focus.length > 0 ? focus[0] + n : 0, loop, filter);18171818if (index > -1) {1819this.setFocus([index], browserEvent);1820}1821}18221823focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1824if (this.length === 0) { return; }18251826const focus = this.focus.get();1827const index = this.findPreviousIndex(focus.length > 0 ? focus[0] - n : 0, loop, filter);18281829if (index > -1) {1830this.setFocus([index], browserEvent);1831}1832}18331834async focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): Promise<void> {1835let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight);1836lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;1837const currentlyFocusedElementIndex = this.getFocus()[0];18381839if (currentlyFocusedElementIndex !== lastPageIndex && (currentlyFocusedElementIndex === undefined || lastPageIndex > currentlyFocusedElementIndex)) {1840const lastGoodPageIndex = this.findPreviousIndex(lastPageIndex, false, filter);18411842if (lastGoodPageIndex > -1 && currentlyFocusedElementIndex !== lastGoodPageIndex) {1843this.setFocus([lastGoodPageIndex], browserEvent);1844} else {1845this.setFocus([lastPageIndex], browserEvent);1846}1847} else {1848const previousScrollTop = this.view.getScrollTop();1849let nextpageScrollTop = previousScrollTop + this.view.renderHeight;1850if (lastPageIndex > currentlyFocusedElementIndex) {1851// scroll last page element to the top only if the last page element is below the focused element1852nextpageScrollTop -= this.view.elementHeight(lastPageIndex);1853}18541855this.view.setScrollTop(nextpageScrollTop);18561857if (this.view.getScrollTop() !== previousScrollTop) {1858this.setFocus([]);18591860// Let the scroll event listener run1861await timeout(0);1862await this.focusNextPage(browserEvent, filter);1863}1864}1865}18661867async focusPreviousPage(browserEvent?: UIEvent, filter?: (element: T) => boolean, getPaddingTop: () => number = () => 0): Promise<void> {1868let firstPageIndex: number;1869const paddingTop = getPaddingTop();1870const scrollTop = this.view.getScrollTop() + paddingTop;18711872if (scrollTop === 0) {1873firstPageIndex = this.view.indexAt(scrollTop);1874} else {1875firstPageIndex = this.view.indexAfter(scrollTop - 1);1876}18771878const currentlyFocusedElementIndex = this.getFocus()[0];18791880if (currentlyFocusedElementIndex !== firstPageIndex && (currentlyFocusedElementIndex === undefined || currentlyFocusedElementIndex >= firstPageIndex)) {1881const firstGoodPageIndex = this.findNextIndex(firstPageIndex, false, filter);18821883if (firstGoodPageIndex > -1 && currentlyFocusedElementIndex !== firstGoodPageIndex) {1884this.setFocus([firstGoodPageIndex], browserEvent);1885} else {1886this.setFocus([firstPageIndex], browserEvent);1887}1888} else {1889const previousScrollTop = scrollTop;1890this.view.setScrollTop(scrollTop - this.view.renderHeight - paddingTop);18911892if (this.view.getScrollTop() + getPaddingTop() !== previousScrollTop) {1893this.setFocus([]);18941895// Let the scroll event listener run1896await timeout(0);1897await this.focusPreviousPage(browserEvent, filter, getPaddingTop);1898}1899}1900}19011902focusLast(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1903if (this.length === 0) { return; }19041905const index = this.findPreviousIndex(this.length - 1, false, filter);19061907if (index > -1) {1908this.setFocus([index], browserEvent);1909}1910}19111912focusFirst(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1913this.focusNth(0, browserEvent, filter);1914}19151916focusNth(n: number, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1917if (this.length === 0) { return; }19181919const index = this.findNextIndex(n, false, filter);19201921if (index > -1) {1922this.setFocus([index], browserEvent);1923}1924}19251926private findNextIndex(index: number, loop = false, filter?: (element: T) => boolean): number {1927for (let i = 0; i < this.length; i++) {1928if (index >= this.length && !loop) {1929return -1;1930}19311932index = index % this.length;19331934if (!filter || filter(this.element(index))) {1935return index;1936}19371938index++;1939}19401941return -1;1942}19431944private findPreviousIndex(index: number, loop = false, filter?: (element: T) => boolean): number {1945for (let i = 0; i < this.length; i++) {1946if (index < 0 && !loop) {1947return -1;1948}19491950index = (this.length + (index % this.length)) % this.length;19511952if (!filter || filter(this.element(index))) {1953return index;1954}19551956index--;1957}19581959return -1;1960}19611962getFocus(): number[] {1963return this.focus.get();1964}19651966getFocusedElements(): T[] {1967return this.getFocus().map(i => this.view.element(i));1968}19691970reveal(index: number, relativeTop?: number, paddingTop: number = 0): void {1971if (index < 0 || index >= this.length) {1972throw new ListError(this.user, `Invalid index ${index}`);1973}19741975const scrollTop = this.view.getScrollTop();1976const elementTop = this.view.elementTop(index);1977const elementHeight = this.view.elementHeight(index);19781979if (isNumber(relativeTop)) {1980// y = mx + b1981const m = elementHeight - this.view.renderHeight + paddingTop;1982this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop - paddingTop);1983} else {1984const viewItemBottom = elementTop + elementHeight;1985const scrollBottom = scrollTop + this.view.renderHeight;19861987if (elementTop < scrollTop + paddingTop && viewItemBottom >= scrollBottom) {1988// The element is already overflowing the viewport, no-op1989} else if (elementTop < scrollTop + paddingTop || (viewItemBottom >= scrollBottom && elementHeight >= this.view.renderHeight)) {1990this.view.setScrollTop(elementTop - paddingTop);1991} else if (viewItemBottom >= scrollBottom) {1992this.view.setScrollTop(viewItemBottom - this.view.renderHeight);1993}1994}1995}19961997/**1998* Returns the relative position of an element rendered in the list.1999* Returns `null` if the element isn't *entirely* in the visible viewport.2000*/2001getRelativeTop(index: number, paddingTop: number = 0): number | null {2002if (index < 0 || index >= this.length) {2003throw new ListError(this.user, `Invalid index ${index}`);2004}20052006const scrollTop = this.view.getScrollTop();2007const elementTop = this.view.elementTop(index);2008const elementHeight = this.view.elementHeight(index);20092010if (elementTop < scrollTop + paddingTop || elementTop + elementHeight > scrollTop + this.view.renderHeight) {2011return null;2012}20132014// y = mx + b2015const m = elementHeight - this.view.renderHeight + paddingTop;2016return Math.abs((scrollTop + paddingTop - elementTop) / m);2017}20182019isDOMFocused(): boolean {2020return isActiveElement(this.view.domNode);2021}20222023getHTMLElement(): HTMLElement {2024return this.view.domNode;2025}20262027getScrollableElement(): HTMLElement {2028return this.view.scrollableElementDomNode;2029}20302031getElementID(index: number): string {2032return this.view.getElementDomId(index);2033}20342035getElementTop(index: number): number {2036return this.view.elementTop(index);2037}20382039style(styles: IListStyles): void {2040this.styleController.style(styles);2041}20422043delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {2044this.view.delegateScrollFromMouseWheelEvent(browserEvent);2045}20462047private toListEvent({ indexes, browserEvent }: ITraitChangeEvent) {2048return { indexes, elements: indexes.map(i => this.view.element(i)), browserEvent };2049}20502051private _onFocusChange(): void {2052const focus = this.focus.get();2053this.view.domNode.classList.toggle('element-focused', focus.length > 0);2054this.onDidChangeActiveDescendant();2055}20562057private onDidChangeActiveDescendant(): void {2058const focus = this.focus.get();20592060if (focus.length > 0) {2061let id: string | undefined;20622063if (this.accessibilityProvider?.getActiveDescendantId) {2064id = this.accessibilityProvider.getActiveDescendantId(this.view.element(focus[0]));2065}20662067this.view.domNode.setAttribute('aria-activedescendant', id || this.view.getElementDomId(focus[0]));2068} else {2069this.view.domNode.removeAttribute('aria-activedescendant');2070}2071}20722073private _onSelectionChange(): void {2074const selection = this.selection.get();20752076this.view.domNode.classList.toggle('selection-none', selection.length === 0);2077this.view.domNode.classList.toggle('selection-single', selection.length === 1);2078this.view.domNode.classList.toggle('selection-multiple', selection.length > 1);2079}20802081dispose(): void {2082this._onDidDispose.fire();2083this.disposables.dispose();20842085this._onDidDispose.dispose();2086}2087}208820892090