Path: blob/main/src/vs/base/browser/ui/list/listWidget.ts
3296 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 } 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();405this.list.setSelection(range(this.list.length), e.browserEvent);406this.list.setAnchor(undefined);407this.view.domNode.focus();408}409410private onEscape(e: StandardKeyboardEvent): void {411if (this.list.getSelection().length) {412e.preventDefault();413e.stopPropagation();414this.list.setSelection([], e.browserEvent);415this.list.setAnchor(undefined);416this.view.domNode.focus();417}418}419420dispose() {421this.disposables.dispose();422this.multipleSelectionDisposables.dispose();423}424}425426export enum TypeNavigationMode {427Automatic,428Trigger429}430431enum TypeNavigationControllerState {432Idle,433Typing434}435436export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardNavigationDelegate {437mightProducePrintableCharacter(event: IKeyboardEvent): boolean {438if (event.ctrlKey || event.metaKey || event.altKey) {439return false;440}441442return (event.keyCode >= KeyCode.KeyA && event.keyCode <= KeyCode.KeyZ)443|| (event.keyCode >= KeyCode.Digit0 && event.keyCode <= KeyCode.Digit9)444|| (event.keyCode >= KeyCode.Numpad0 && event.keyCode <= KeyCode.Numpad9)445|| (event.keyCode >= KeyCode.Semicolon && event.keyCode <= KeyCode.Quote);446}447};448449class TypeNavigationController<T> implements IDisposable {450451private enabled = false;452private state: TypeNavigationControllerState = TypeNavigationControllerState.Idle;453454private mode = TypeNavigationMode.Automatic;455private triggered = false;456private previouslyFocused = -1;457458private readonly enabledDisposables = new DisposableStore();459private readonly disposables = new DisposableStore();460461constructor(462private list: List<T>,463private view: IListView<T>,464private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>,465private keyboardNavigationEventFilter: IKeyboardNavigationEventFilter,466private delegate: IKeyboardNavigationDelegate467) {468this.updateOptions(list.options);469}470471updateOptions(options: IListOptions<T>): void {472if (options.typeNavigationEnabled ?? true) {473this.enable();474} else {475this.disable();476}477478this.mode = options.typeNavigationMode ?? TypeNavigationMode.Automatic;479}480481trigger(): void {482this.triggered = !this.triggered;483}484485private enable(): void {486if (this.enabled) {487return;488}489490let typing = false;491492const onChar = Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>493$.filter(e => !isEditableElement(e.target as HTMLElement))494.filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered)495.map(event => new StandardKeyboardEvent(event))496.filter(e => typing || this.keyboardNavigationEventFilter(e))497.filter(e => this.delegate.mightProducePrintableCharacter(e))498.forEach(e => EventHelper.stop(e, true))499.map(event => event.browserEvent.key)500);501502const onClear = Event.debounce<string, null>(onChar, () => null, 800, undefined, undefined, undefined, this.enabledDisposables);503const onInput = Event.reduce<string | null, string | null>(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i), undefined, this.enabledDisposables);504505onInput(this.onInput, this, this.enabledDisposables);506onClear(this.onClear, this, this.enabledDisposables);507508onChar(() => typing = true, undefined, this.enabledDisposables);509onClear(() => typing = false, undefined, this.enabledDisposables);510511this.enabled = true;512this.triggered = false;513}514515private disable(): void {516if (!this.enabled) {517return;518}519520this.enabledDisposables.clear();521this.enabled = false;522this.triggered = false;523}524525private onClear(): void {526const focus = this.list.getFocus();527if (focus.length > 0 && focus[0] === this.previouslyFocused) {528// List: re-announce element on typing end since typed keys will interrupt aria label of focused element529// Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961530const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0]));531532if (typeof ariaLabel === 'string') {533alert(ariaLabel);534} else if (ariaLabel) {535alert(ariaLabel.get());536}537}538this.previouslyFocused = -1;539}540541private onInput(word: string | null): void {542if (!word) {543this.state = TypeNavigationControllerState.Idle;544this.triggered = false;545return;546}547548const focus = this.list.getFocus();549const start = focus.length > 0 ? focus[0] : 0;550const delta = this.state === TypeNavigationControllerState.Idle ? 1 : 0;551this.state = TypeNavigationControllerState.Typing;552553for (let i = 0; i < this.list.length; i++) {554const index = (start + i + delta) % this.list.length;555const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(this.view.element(index));556const labelStr = label && label.toString();557558if (this.list.options.typeNavigationEnabled) {559if (typeof labelStr !== 'undefined') {560561// If prefix is found, focus and return early562if (matchesPrefix(word, labelStr)) {563this.previouslyFocused = start;564this.list.setFocus([index]);565this.list.reveal(index);566return;567}568569const fuzzy = matchesFuzzy2(word, labelStr);570571if (fuzzy) {572const fuzzyScore = fuzzy[0].end - fuzzy[0].start;573// 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.574if (fuzzyScore > 1 && fuzzy.length === 1) {575this.previouslyFocused = start;576this.list.setFocus([index]);577this.list.reveal(index);578return;579}580}581}582} else if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) {583this.previouslyFocused = start;584this.list.setFocus([index]);585this.list.reveal(index);586return;587}588}589}590591dispose() {592this.disable();593this.enabledDisposables.dispose();594this.disposables.dispose();595}596}597598class DOMFocusController<T> implements IDisposable {599600private readonly disposables = new DisposableStore();601602constructor(603private list: List<T>,604private view: IListView<T>605) {606const onKeyDown = Event.chain(this.disposables.add(new DomEmitter(view.domNode, 'keydown')).event, $ => $607.filter(e => !isEditableElement(e.target as HTMLElement))608.map(e => new StandardKeyboardEvent(e))609);610611const onTab = Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey));612613onTab(this.onTab, this, this.disposables);614}615616private onTab(e: StandardKeyboardEvent): void {617if (e.target !== this.view.domNode) {618return;619}620621const focus = this.list.getFocus();622623if (focus.length === 0) {624return;625}626627const focusedDomElement = this.view.domElement(focus[0]);628629if (!focusedDomElement) {630return;631}632633const tabIndexElement = focusedDomElement.querySelector('[tabIndex]');634635if (!tabIndexElement || !(isHTMLElement(tabIndexElement)) || tabIndexElement.tabIndex === -1) {636return;637}638639const style = getWindow(tabIndexElement).getComputedStyle(tabIndexElement);640if (style.visibility === 'hidden' || style.display === 'none') {641return;642}643644e.preventDefault();645e.stopPropagation();646tabIndexElement.focus();647}648649dispose() {650this.disposables.dispose();651}652}653654export function isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {655return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;656}657658export function isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {659return event.browserEvent.shiftKey;660}661662function isMouseRightClick(event: UIEvent): boolean {663return isMouseEvent(event) && event.button === 2;664}665666const DefaultMultipleSelectionController = {667isSelectionSingleChangeEvent,668isSelectionRangeChangeEvent669};670671export class MouseController<T> implements IDisposable {672673private multipleSelectionController: IMultipleSelectionController<T> | undefined;674private readonly mouseSupport: boolean;675private readonly disposables = new DisposableStore();676677private readonly _onPointer = this.disposables.add(new Emitter<IListMouseEvent<T>>());678get onPointer() { return this._onPointer.event; }679680constructor(protected list: List<T>) {681if (list.options.multipleSelectionSupport !== false) {682this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController;683}684685this.mouseSupport = typeof list.options.mouseSupport === 'undefined' || !!list.options.mouseSupport;686687if (this.mouseSupport) {688list.onMouseDown(this.onMouseDown, this, this.disposables);689list.onContextMenu(this.onContextMenu, this, this.disposables);690list.onMouseDblClick(this.onDoubleClick, this, this.disposables);691list.onTouchStart(this.onMouseDown, this, this.disposables);692this.disposables.add(Gesture.addTarget(list.getHTMLElement()));693}694695Event.any<IListMouseEvent<any> | IListGestureEvent<any>>(list.onMouseClick, list.onMouseMiddleClick, list.onTap)(this.onViewPointer, this, this.disposables);696}697698updateOptions(optionsUpdate: IListOptionsUpdate): void {699if (optionsUpdate.multipleSelectionSupport !== undefined) {700this.multipleSelectionController = undefined;701702if (optionsUpdate.multipleSelectionSupport) {703this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController;704}705}706}707708protected isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {709if (!this.multipleSelectionController) {710return false;711}712713return this.multipleSelectionController.isSelectionSingleChangeEvent(event);714}715716protected isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {717if (!this.multipleSelectionController) {718return false;719}720721return this.multipleSelectionController.isSelectionRangeChangeEvent(event);722}723724private isSelectionChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {725return this.isSelectionSingleChangeEvent(event) || this.isSelectionRangeChangeEvent(event);726}727728protected onMouseDown(e: IListMouseEvent<T> | IListTouchEvent<T>): void {729if (isMonacoEditor(e.browserEvent.target as HTMLElement)) {730return;731}732733if (getActiveElement() !== e.browserEvent.target) {734this.list.domFocus();735}736}737738protected onContextMenu(e: IListContextMenuEvent<T>): void {739if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {740return;741}742743const focus = typeof e.index === 'undefined' ? [] : [e.index];744this.list.setFocus(focus, e.browserEvent);745}746747protected onViewPointer(e: IListMouseEvent<T>): void {748if (!this.mouseSupport) {749return;750}751752if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {753return;754}755756if (e.browserEvent.isHandledByList) {757return;758}759760e.browserEvent.isHandledByList = true;761const focus = e.index;762763if (typeof focus === 'undefined') {764this.list.setFocus([], e.browserEvent);765this.list.setSelection([], e.browserEvent);766this.list.setAnchor(undefined);767return;768}769770if (this.isSelectionChangeEvent(e)) {771return this.changeSelection(e);772}773774this.list.setFocus([focus], e.browserEvent);775this.list.setAnchor(focus);776777if (!isMouseRightClick(e.browserEvent)) {778this.list.setSelection([focus], e.browserEvent);779}780781this._onPointer.fire(e);782}783784protected onDoubleClick(e: IListMouseEvent<T>): void {785if (isEditableElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {786return;787}788789if (this.isSelectionChangeEvent(e)) {790return;791}792793if (e.browserEvent.isHandledByList) {794return;795}796797e.browserEvent.isHandledByList = true;798const focus = this.list.getFocus();799this.list.setSelection(focus, e.browserEvent);800}801802private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>): void {803const focus = e.index!;804let anchor = this.list.getAnchor();805806if (this.isSelectionRangeChangeEvent(e)) {807if (typeof anchor === 'undefined') {808const currentFocus = this.list.getFocus()[0];809anchor = currentFocus ?? focus;810this.list.setAnchor(anchor);811}812813const min = Math.min(anchor, focus);814const max = Math.max(anchor, focus);815const rangeSelection = range(min, max + 1);816const selection = this.list.getSelection();817const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor);818819if (contiguousRange.length === 0) {820return;821}822823const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange));824this.list.setSelection(newSelection, e.browserEvent);825this.list.setFocus([focus], e.browserEvent);826827} else if (this.isSelectionSingleChangeEvent(e)) {828const selection = this.list.getSelection();829const newSelection = selection.filter(i => i !== focus);830831this.list.setFocus([focus]);832this.list.setAnchor(focus);833834if (selection.length === newSelection.length) {835this.list.setSelection([...newSelection, focus], e.browserEvent);836} else {837this.list.setSelection(newSelection, e.browserEvent);838}839}840}841842dispose() {843this.disposables.dispose();844}845}846847export interface IMultipleSelectionController<T> {848isSelectionSingleChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;849isSelectionRangeChangeEvent(event: IListMouseEvent<T> | IListTouchEvent<T>): boolean;850}851852export interface IStyleController {853style(styles: IListStyles): void;854}855856export interface IListAccessibilityProvider<T> extends IListViewAccessibilityProvider<T> {857getAriaLabel(element: T): string | IObservable<string> | null;858getWidgetAriaLabel(): string | IObservable<string>;859getWidgetRole?(): AriaRole;860getAriaLevel?(element: T): number | undefined;861onDidChangeActiveDescendant?: Event<void>;862getActiveDescendantId?(element: T): string | undefined;863}864865export class DefaultStyleController implements IStyleController {866867constructor(private styleElement: HTMLStyleElement, private selectorSuffix: string) { }868869style(styles: IListStyles): void {870const suffix = this.selectorSuffix && `.${this.selectorSuffix}`;871const content: string[] = [];872873if (styles.listBackground) {874content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`);875}876877if (styles.listFocusBackground) {878content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`);879content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case!880}881882if (styles.listFocusForeground) {883content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`);884}885886if (styles.listActiveSelectionBackground) {887content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`);888content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case!889}890891if (styles.listActiveSelectionForeground) {892content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`);893}894895if (styles.listActiveSelectionIconForeground) {896content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; }`);897}898899if (styles.listFocusAndSelectionBackground) {900content.push(`901.monaco-drag-image${suffix},902.monaco-list${suffix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }903`);904}905906if (styles.listFocusAndSelectionForeground) {907content.push(`908.monaco-drag-image${suffix},909.monaco-list${suffix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }910`);911}912913if (styles.listInactiveFocusForeground) {914content.push(`.monaco-list${suffix} .monaco-list-row.focused { color: ${styles.listInactiveFocusForeground}; }`);915content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { color: ${styles.listInactiveFocusForeground}; }`); // overwrite :hover style in this case!916}917918if (styles.listInactiveSelectionIconForeground) {919content.push(`.monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; }`);920}921922if (styles.listInactiveFocusBackground) {923content.push(`.monaco-list${suffix} .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`);924content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!925}926927if (styles.listInactiveSelectionBackground) {928content.push(`.monaco-list${suffix} .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`);929content.push(`.monaco-list${suffix} .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case!930}931932if (styles.listInactiveSelectionForeground) {933content.push(`.monaco-list${suffix} .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`);934}935936if (styles.listHoverBackground) {937content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);938}939940if (styles.listHoverForeground) {941content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`);942}943944/**945* Outlines946*/947const focusAndSelectionOutline = asCssValueWithDefault(styles.listFocusAndSelectionOutline, asCssValueWithDefault(styles.listSelectionOutline, styles.listFocusOutline ?? ''));948if (focusAndSelectionOutline) { // default: listFocusOutline949content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused.selected { outline: 1px solid ${focusAndSelectionOutline}; outline-offset: -1px;}`);950}951952if (styles.listFocusOutline) { // default: set953content.push(`954.monaco-drag-image${suffix},955.monaco-list${suffix}:focus .monaco-list-row.focused,956.monaco-workbench.context-menu-visible .monaco-list${suffix}.last-focused .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }957`);958}959960const inactiveFocusAndSelectionOutline = asCssValueWithDefault(styles.listSelectionOutline, styles.listInactiveFocusOutline ?? '');961if (inactiveFocusAndSelectionOutline) {962content.push(`.monaco-list${suffix} .monaco-list-row.focused.selected { outline: 1px dotted ${inactiveFocusAndSelectionOutline}; outline-offset: -1px; }`);963}964965if (styles.listSelectionOutline) { // default: activeContrastBorder966content.push(`.monaco-list${suffix} .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`);967}968969if (styles.listInactiveFocusOutline) { // default: null970content.push(`.monaco-list${suffix} .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`);971}972973if (styles.listHoverOutline) { // default: activeContrastBorder974content.push(`.monaco-list${suffix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);975}976977if (styles.listDropOverBackground) {978content.push(`979.monaco-list${suffix}.drop-target,980.monaco-list${suffix} .monaco-list-rows.drop-target,981.monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropOverBackground} !important; color: inherit !important; }982`);983}984985if (styles.listDropBetweenBackground) {986content.push(`987.monaco-list${suffix} .monaco-list-rows.drop-target-before .monaco-list-row:first-child::before,988.monaco-list${suffix} .monaco-list-row.drop-target-before::before {989content: ""; position: absolute; top: 0px; left: 0px; width: 100%; height: 1px;990background-color: ${styles.listDropBetweenBackground};991}`);992content.push(`993.monaco-list${suffix} .monaco-list-rows.drop-target-after .monaco-list-row:last-child::after,994.monaco-list${suffix} .monaco-list-row.drop-target-after::after {995content: ""; position: absolute; bottom: 0px; left: 0px; width: 100%; height: 1px;996background-color: ${styles.listDropBetweenBackground};997}`);998}9991000if (styles.tableColumnsBorder) {1001content.push(`1002.monaco-table > .monaco-split-view2,1003.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before,1004.monaco-workbench:not(.reduce-motion) .monaco-table:hover > .monaco-split-view2,1005.monaco-workbench:not(.reduce-motion) .monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before {1006border-color: ${styles.tableColumnsBorder};1007}10081009.monaco-workbench:not(.reduce-motion) .monaco-table > .monaco-split-view2,1010.monaco-workbench:not(.reduce-motion) .monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {1011border-color: transparent;1012}1013`);1014}10151016if (styles.tableOddRowsBackgroundColor) {1017content.push(`1018.monaco-table .monaco-list-row[data-parity=odd]:not(.focused):not(.selected):not(:hover) .monaco-table-tr,1019.monaco-table .monaco-list:not(:focus) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr,1020.monaco-table .monaco-list:not(.focused) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr {1021background-color: ${styles.tableOddRowsBackgroundColor};1022}1023`);1024}10251026this.styleElement.textContent = content.join('\n');1027}1028}10291030export interface IKeyboardNavigationEventFilter {1031(e: StandardKeyboardEvent): boolean;1032}10331034export interface IListOptionsUpdate extends IListViewOptionsUpdate {1035readonly typeNavigationEnabled?: boolean;1036readonly typeNavigationMode?: TypeNavigationMode;1037readonly multipleSelectionSupport?: boolean;1038}10391040export interface IListOptions<T> extends IListOptionsUpdate {1041readonly identityProvider?: IIdentityProvider<T>;1042readonly dnd?: IListDragAndDrop<T>;1043readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider<T>;1044readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate;1045readonly keyboardSupport?: boolean;1046readonly multipleSelectionController?: IMultipleSelectionController<T>;1047readonly styleController?: (suffix: string) => IStyleController;1048readonly accessibilityProvider?: IListAccessibilityProvider<T>;1049readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;10501051// list view options1052readonly useShadows?: boolean;1053readonly verticalScrollMode?: ScrollbarVisibility;1054readonly setRowLineHeight?: boolean;1055readonly setRowHeight?: boolean;1056readonly supportDynamicHeights?: boolean;1057readonly mouseSupport?: boolean;1058readonly userSelection?: boolean;1059readonly horizontalScrolling?: boolean;1060readonly scrollByPage?: boolean;1061readonly transformOptimization?: boolean;1062readonly smoothScrolling?: boolean;1063readonly scrollableElementChangeOptions?: ScrollableElementChangeOptions;1064readonly alwaysConsumeMouseWheel?: boolean;1065readonly initialSize?: Dimension;1066readonly paddingTop?: number;1067readonly paddingBottom?: number;1068}10691070export interface IListStyles {1071listBackground: string | undefined;1072listFocusBackground: string | undefined;1073listFocusForeground: string | undefined;1074listActiveSelectionBackground: string | undefined;1075listActiveSelectionForeground: string | undefined;1076listActiveSelectionIconForeground: string | undefined;1077listFocusAndSelectionOutline: string | undefined;1078listFocusAndSelectionBackground: string | undefined;1079listFocusAndSelectionForeground: string | undefined;1080listInactiveSelectionBackground: string | undefined;1081listInactiveSelectionIconForeground: string | undefined;1082listInactiveSelectionForeground: string | undefined;1083listInactiveFocusForeground: string | undefined;1084listInactiveFocusBackground: string | undefined;1085listHoverBackground: string | undefined;1086listHoverForeground: string | undefined;1087listDropOverBackground: string | undefined;1088listDropBetweenBackground: string | undefined;1089listFocusOutline: string | undefined;1090listInactiveFocusOutline: string | undefined;1091listSelectionOutline: string | undefined;1092listHoverOutline: string | undefined;1093treeIndentGuidesStroke: string | undefined;1094treeInactiveIndentGuidesStroke: string | undefined;1095treeStickyScrollBackground: string | undefined;1096treeStickyScrollBorder: string | undefined;1097treeStickyScrollShadow: string | undefined;1098tableColumnsBorder: string | undefined;1099tableOddRowsBackgroundColor: string | undefined;1100}11011102export const unthemedListStyles: IListStyles = {1103listFocusBackground: '#7FB0D0',1104listActiveSelectionBackground: '#0E639C',1105listActiveSelectionForeground: '#FFFFFF',1106listActiveSelectionIconForeground: '#FFFFFF',1107listFocusAndSelectionOutline: '#90C2F9',1108listFocusAndSelectionBackground: '#094771',1109listFocusAndSelectionForeground: '#FFFFFF',1110listInactiveSelectionBackground: '#3F3F46',1111listInactiveSelectionIconForeground: '#FFFFFF',1112listHoverBackground: '#2A2D2E',1113listDropOverBackground: '#383B3D',1114listDropBetweenBackground: '#EEEEEE',1115treeIndentGuidesStroke: '#a9a9a9',1116treeInactiveIndentGuidesStroke: Color.fromHex('#a9a9a9').transparent(0.4).toString(),1117tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2).toString(),1118tableOddRowsBackgroundColor: Color.fromHex('#cccccc').transparent(0.04).toString(),1119listBackground: undefined,1120listFocusForeground: undefined,1121listInactiveSelectionForeground: undefined,1122listInactiveFocusForeground: undefined,1123listInactiveFocusBackground: undefined,1124listHoverForeground: undefined,1125listFocusOutline: undefined,1126listInactiveFocusOutline: undefined,1127listSelectionOutline: undefined,1128listHoverOutline: undefined,1129treeStickyScrollBackground: undefined,1130treeStickyScrollBorder: undefined,1131treeStickyScrollShadow: undefined1132};11331134const DefaultOptions: IListOptions<any> = {1135keyboardSupport: true,1136mouseSupport: true,1137multipleSelectionSupport: true,1138dnd: {1139getDragURI() { return null; },1140onDragStart(): void { },1141onDragOver() { return false; },1142drop() { },1143dispose() { }1144}1145};11461147// TODO@Joao: move these utils into a SortedArray class11481149function getContiguousRangeContaining(range: number[], value: number): number[] {1150const index = range.indexOf(value);11511152if (index === -1) {1153return [];1154}11551156const result: number[] = [];1157let i = index - 1;1158while (i >= 0 && range[i] === value - (index - i)) {1159result.push(range[i--]);1160}11611162result.reverse();1163i = index;1164while (i < range.length && range[i] === value + (i - index)) {1165result.push(range[i++]);1166}11671168return result;1169}11701171/**1172* Given two sorted collections of numbers, returns the intersection1173* between them (OR).1174*/1175function disjunction(one: number[], other: number[]): number[] {1176const result: number[] = [];1177let i = 0, j = 0;11781179while (i < one.length || j < other.length) {1180if (i >= one.length) {1181result.push(other[j++]);1182} else if (j >= other.length) {1183result.push(one[i++]);1184} else if (one[i] === other[j]) {1185result.push(one[i]);1186i++;1187j++;1188continue;1189} else if (one[i] < other[j]) {1190result.push(one[i++]);1191} else {1192result.push(other[j++]);1193}1194}11951196return result;1197}11981199/**1200* Given two sorted collections of numbers, returns the relative1201* complement between them (XOR).1202*/1203function relativeComplement(one: number[], other: number[]): number[] {1204const result: number[] = [];1205let i = 0, j = 0;12061207while (i < one.length || j < other.length) {1208if (i >= one.length) {1209result.push(other[j++]);1210} else if (j >= other.length) {1211result.push(one[i++]);1212} else if (one[i] === other[j]) {1213i++;1214j++;1215continue;1216} else if (one[i] < other[j]) {1217result.push(one[i++]);1218} else {1219j++;1220}1221}12221223return result;1224}12251226const numericSort = (a: number, b: number) => a - b;12271228class PipelineRenderer<T> implements IListRenderer<T, any> {12291230constructor(1231private _templateId: string,1232private renderers: IListRenderer<any /* TODO@joao */, any>[]1233) { }12341235get templateId(): string {1236return this._templateId;1237}12381239renderTemplate(container: HTMLElement): any[] {1240return this.renderers.map(r => r.renderTemplate(container));1241}12421243renderElement(element: T, index: number, templateData: any[], renderDetails?: IListElementRenderDetails): void {1244let i = 0;12451246for (const renderer of this.renderers) {1247renderer.renderElement(element, index, templateData[i++], renderDetails);1248}1249}12501251disposeElement(element: T, index: number, templateData: any[], renderDetails?: IListElementRenderDetails): void {1252let i = 0;12531254for (const renderer of this.renderers) {1255renderer.disposeElement?.(element, index, templateData[i], renderDetails);12561257i += 1;1258}1259}12601261disposeTemplate(templateData: unknown[]): void {1262let i = 0;12631264for (const renderer of this.renderers) {1265renderer.disposeTemplate(templateData[i++]);1266}1267}1268}12691270class AccessibiltyRenderer<T> implements IListRenderer<T, IAccessibilityTemplateData> {12711272templateId: string = 'a18n';12731274constructor(private accessibilityProvider: IListAccessibilityProvider<T>) { }12751276renderTemplate(container: HTMLElement): IAccessibilityTemplateData {1277return { container, disposables: new DisposableStore() };1278}12791280renderElement(element: T, index: number, data: IAccessibilityTemplateData): void {1281const ariaLabel = this.accessibilityProvider.getAriaLabel(element);1282const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel);12831284data.disposables.add(autorun(reader => {1285this.setAriaLabel(reader.readObservable(observable), data.container);1286}));12871288const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element);12891290if (typeof ariaLevel === 'number') {1291data.container.setAttribute('aria-level', `${ariaLevel}`);1292} else {1293data.container.removeAttribute('aria-level');1294}1295}12961297private setAriaLabel(ariaLabel: string | null, element: HTMLElement): void {1298if (ariaLabel) {1299element.setAttribute('aria-label', ariaLabel);1300} else {1301element.removeAttribute('aria-label');1302}1303}13041305disposeElement(element: T, index: number, templateData: IAccessibilityTemplateData): void {1306templateData.disposables.clear();1307}13081309disposeTemplate(templateData: IAccessibilityTemplateData): void {1310templateData.disposables.dispose();1311}1312}13131314class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {13151316constructor(private list: List<T>, private dnd: IListDragAndDrop<T>) { }13171318getDragElements(element: T): T[] {1319const selection = this.list.getSelectedElements();1320const elements = selection.indexOf(element) > -1 ? selection : [element];1321return elements;1322}13231324getDragURI(element: T): string | null {1325return this.dnd.getDragURI(element);1326}13271328getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined {1329if (this.dnd.getDragLabel) {1330return this.dnd.getDragLabel(elements, originalEvent);1331}13321333return undefined;1334}13351336onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {1337this.dnd.onDragStart?.(data, originalEvent);1338}13391340onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction {1341return this.dnd.onDragOver(data, targetElement, targetIndex, targetSector, originalEvent);1342}13431344onDragLeave(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {1345this.dnd.onDragLeave?.(data, targetElement, targetIndex, originalEvent);1346}13471348onDragEnd(originalEvent: DragEvent): void {1349this.dnd.onDragEnd?.(originalEvent);1350}13511352drop(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void {1353this.dnd.drop(data, targetElement, targetIndex, targetSector, originalEvent);1354}13551356dispose(): void {1357this.dnd.dispose();1358}1359}13601361/**1362* The {@link List} is a virtual scrolling widget, built on top of the {@link ListView}1363* widget.1364*1365* Features:1366* - Customizable keyboard and mouse support1367* - Element traits: focus, selection, achor1368* - Accessibility support1369* - Touch support1370* - Performant template-based rendering1371* - Horizontal scrolling1372* - Variable element height support1373* - Dynamic element height support1374* - Drag-and-drop support1375*/1376export class List<T> implements ISpliceable<T>, IDisposable {13771378private focus = new Trait<T>('focused');1379private selection: Trait<T>;1380private anchor = new Trait<T>('anchor');1381private eventBufferer = new EventBufferer();1382protected view: IListView<T>;1383private spliceable: ISpliceable<T>;1384private styleController: IStyleController;1385private typeNavigationController?: TypeNavigationController<T>;1386private accessibilityProvider?: IListAccessibilityProvider<T>;1387private keyboardController: KeyboardController<T> | undefined;1388private mouseController: MouseController<T>;1389private _ariaLabel: string = '';13901391protected readonly disposables = new DisposableStore();13921393@memoize get onDidChangeFocus(): Event<IListEvent<T>> {1394return Event.map(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toListEvent(e), this.disposables);1395}13961397@memoize get onDidChangeSelection(): Event<IListEvent<T>> {1398return Event.map(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toListEvent(e), this.disposables);1399}14001401get domId(): string { return this.view.domId; }1402get onDidScroll(): Event<ScrollEvent> { return this.view.onDidScroll; }1403get onMouseClick(): Event<IListMouseEvent<T>> { return this.view.onMouseClick; }1404get onMouseDblClick(): Event<IListMouseEvent<T>> { return this.view.onMouseDblClick; }1405get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return this.view.onMouseMiddleClick; }1406get onPointer(): Event<IListMouseEvent<T>> { return this.mouseController.onPointer; }1407get onMouseUp(): Event<IListMouseEvent<T>> { return this.view.onMouseUp; }1408get onMouseDown(): Event<IListMouseEvent<T>> { return this.view.onMouseDown; }1409get onMouseOver(): Event<IListMouseEvent<T>> { return this.view.onMouseOver; }1410get onMouseMove(): Event<IListMouseEvent<T>> { return this.view.onMouseMove; }1411get onMouseOut(): Event<IListMouseEvent<T>> { return this.view.onMouseOut; }1412get onTouchStart(): Event<IListTouchEvent<T>> { return this.view.onTouchStart; }1413get onTap(): Event<IListGestureEvent<T>> { return this.view.onTap; }14141415/**1416* Possible context menu trigger events:1417* - ContextMenu key1418* - Shift F101419* - Ctrl Option Shift M (macOS with VoiceOver)1420* - Mouse right click1421*/1422@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {1423let didJustPressContextMenuKey = false;14241425const fromKeyDown: Event<any> = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>1426$.map(e => new StandardKeyboardEvent(e))1427.filter(e => didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))1428.map(e => EventHelper.stop(e, true))1429.filter(() => false));14301431const fromKeyUp = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event, $ =>1432$.forEach(() => didJustPressContextMenuKey = false)1433.map(e => new StandardKeyboardEvent(e))1434.filter(e => e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))1435.map(e => EventHelper.stop(e, true))1436.map(({ browserEvent }) => {1437const focus = this.getFocus();1438const index = focus.length ? focus[0] : undefined;1439const element = typeof index !== 'undefined' ? this.view.element(index) : undefined;1440const anchor = typeof index !== 'undefined' ? this.view.domElement(index) as HTMLElement : this.view.domNode;1441return { index, element, anchor, browserEvent };1442}));14431444const fromMouse = Event.chain(this.view.onContextMenu, $ =>1445$.filter(_ => !didJustPressContextMenuKey)1446.map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(getWindow(this.view.domNode), browserEvent), browserEvent }))1447);14481449return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);1450}14511452@memoize get onKeyDown(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event; }1453@memoize get onKeyUp(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event; }1454@memoize get onKeyPress(): Event<KeyboardEvent> { return this.disposables.add(new DomEmitter(this.view.domNode, 'keypress')).event; }14551456@memoize get onDidFocus(): Event<void> { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'focus', true)).event); }1457@memoize get onDidBlur(): Event<void> { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'blur', true)).event); }14581459private readonly _onDidDispose = new Emitter<void>();1460readonly onDidDispose: Event<void> = this._onDidDispose.event;14611462constructor(1463private user: string,1464container: HTMLElement,1465virtualDelegate: IListVirtualDelegate<T>,1466renderers: IListRenderer<any /* TODO@joao */, any>[],1467private _options: IListOptions<T> = DefaultOptions1468) {1469const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list';1470this.selection = new SelectionTrait(role !== 'listbox');14711472const baseRenderers: IListRenderer<T, unknown>[] = [this.focus.renderer, this.selection.renderer];14731474this.accessibilityProvider = _options.accessibilityProvider;14751476if (this.accessibilityProvider) {1477baseRenderers.push(new AccessibiltyRenderer<T>(this.accessibilityProvider));14781479this.accessibilityProvider.onDidChangeActiveDescendant?.(this.onDidChangeActiveDescendant, this, this.disposables);1480}14811482renderers = renderers.map(r => new PipelineRenderer(r.templateId, [...baseRenderers, r]));14831484const viewOptions: IListViewOptions<T> = {1485..._options,1486dnd: _options.dnd && new ListViewDragAndDrop(this, _options.dnd)1487};14881489this.view = this.createListView(container, virtualDelegate, renderers, viewOptions);1490this.view.domNode.setAttribute('role', role);14911492if (_options.styleController) {1493this.styleController = _options.styleController(this.view.domId);1494} else {1495const styleElement = createStyleSheet(this.view.domNode);1496this.styleController = new DefaultStyleController(styleElement, this.view.domId);1497}14981499this.spliceable = new CombinedSpliceable([1500new TraitSpliceable(this.focus, this.view, _options.identityProvider),1501new TraitSpliceable(this.selection, this.view, _options.identityProvider),1502new TraitSpliceable(this.anchor, this.view, _options.identityProvider),1503this.view1504]);15051506this.disposables.add(this.focus);1507this.disposables.add(this.selection);1508this.disposables.add(this.anchor);1509this.disposables.add(this.view);1510this.disposables.add(this._onDidDispose);15111512this.disposables.add(new DOMFocusController(this, this.view));15131514if (typeof _options.keyboardSupport !== 'boolean' || _options.keyboardSupport) {1515this.keyboardController = new KeyboardController(this, this.view, _options);1516this.disposables.add(this.keyboardController);1517}15181519if (_options.keyboardNavigationLabelProvider) {1520const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;1521this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, _options.keyboardNavigationEventFilter ?? (() => true), delegate);1522this.disposables.add(this.typeNavigationController);1523}15241525this.mouseController = this.createMouseController(_options);1526this.disposables.add(this.mouseController);15271528this.onDidChangeFocus(this._onFocusChange, this, this.disposables);1529this.onDidChangeSelection(this._onSelectionChange, this, this.disposables);15301531if (this.accessibilityProvider) {1532const ariaLabel = this.accessibilityProvider.getWidgetAriaLabel();1533const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel);15341535this.disposables.add(autorun(reader => {1536this.ariaLabel = reader.readObservable(observable);1537}));1538}15391540if (this._options.multipleSelectionSupport !== false) {1541this.view.domNode.setAttribute('aria-multiselectable', 'true');1542}1543}15441545protected createListView(container: HTMLElement, virtualDelegate: IListVirtualDelegate<T>, renderers: IListRenderer<any, any>[], viewOptions: IListViewOptions<T>): IListView<T> {1546return new ListView(container, virtualDelegate, renderers, viewOptions);1547}15481549protected createMouseController(options: IListOptions<T>): MouseController<T> {1550return new MouseController(this);1551}15521553updateOptions(optionsUpdate: IListOptionsUpdate = {}): void {1554this._options = { ...this._options, ...optionsUpdate };15551556this.typeNavigationController?.updateOptions(this._options);15571558if (this._options.multipleSelectionController !== undefined) {1559if (this._options.multipleSelectionSupport) {1560this.view.domNode.setAttribute('aria-multiselectable', 'true');1561} else {1562this.view.domNode.removeAttribute('aria-multiselectable');1563}1564}15651566this.mouseController.updateOptions(optionsUpdate);1567this.keyboardController?.updateOptions(optionsUpdate);1568this.view.updateOptions(optionsUpdate);1569}15701571get options(): IListOptions<T> {1572return this._options;1573}15741575splice(start: number, deleteCount: number, elements: readonly T[] = []): void {1576if (start < 0 || start > this.view.length) {1577throw new ListError(this.user, `Invalid start index: ${start}`);1578}15791580if (deleteCount < 0) {1581throw new ListError(this.user, `Invalid delete count: ${deleteCount}`);1582}15831584if (deleteCount === 0 && elements.length === 0) {1585return;1586}15871588this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements));1589}15901591updateWidth(index: number): void {1592this.view.updateWidth(index);1593}15941595updateElementHeight(index: number, size: number | undefined): void {1596this.view.updateElementHeight(index, size, null);1597}15981599rerender(): void {1600this.view.rerender();1601}16021603element(index: number): T {1604return this.view.element(index);1605}16061607indexOf(element: T): number {1608return this.view.indexOf(element);1609}16101611indexAt(position: number): number {1612return this.view.indexAt(position);1613}16141615get length(): number {1616return this.view.length;1617}16181619get contentHeight(): number {1620return this.view.contentHeight;1621}16221623get contentWidth(): number {1624return this.view.contentWidth;1625}16261627get onDidChangeContentHeight(): Event<number> {1628return this.view.onDidChangeContentHeight;1629}16301631get onDidChangeContentWidth(): Event<number> {1632return this.view.onDidChangeContentWidth;1633}16341635get scrollTop(): number {1636return this.view.getScrollTop();1637}16381639set scrollTop(scrollTop: number) {1640this.view.setScrollTop(scrollTop);1641}16421643get scrollLeft(): number {1644return this.view.getScrollLeft();1645}16461647set scrollLeft(scrollLeft: number) {1648this.view.setScrollLeft(scrollLeft);1649}16501651get scrollHeight(): number {1652return this.view.scrollHeight;1653}16541655get renderHeight(): number {1656return this.view.renderHeight;1657}16581659get firstVisibleIndex(): number {1660return this.view.firstVisibleIndex;1661}16621663get firstMostlyVisibleIndex(): number {1664return this.view.firstMostlyVisibleIndex;1665}16661667get lastVisibleIndex(): number {1668return this.view.lastVisibleIndex;1669}16701671get ariaLabel(): string {1672return this._ariaLabel;1673}16741675set ariaLabel(value: string) {1676this._ariaLabel = value;1677this.view.domNode.setAttribute('aria-label', value);1678}16791680domFocus(): void {1681this.view.domNode.focus({ preventScroll: true });1682}16831684layout(height?: number, width?: number): void {1685this.view.layout(height, width);1686}16871688triggerTypeNavigation(): void {1689this.typeNavigationController?.trigger();1690}16911692setSelection(indexes: number[], browserEvent?: UIEvent): void {1693for (const index of indexes) {1694if (index < 0 || index >= this.length) {1695throw new ListError(this.user, `Invalid index ${index}`);1696}1697}16981699this.selection.set(indexes, browserEvent);1700}17011702getSelection(): number[] {1703return this.selection.get();1704}17051706getSelectedElements(): T[] {1707return this.getSelection().map(i => this.view.element(i));1708}17091710setAnchor(index: number | undefined): void {1711if (typeof index === 'undefined') {1712this.anchor.set([]);1713return;1714}17151716if (index < 0 || index >= this.length) {1717throw new ListError(this.user, `Invalid index ${index}`);1718}17191720this.anchor.set([index]);1721}17221723getAnchor(): number | undefined {1724return this.anchor.get().at(0);1725}17261727getAnchorElement(): T | undefined {1728const anchor = this.getAnchor();1729return typeof anchor === 'undefined' ? undefined : this.element(anchor);1730}17311732setFocus(indexes: number[], browserEvent?: UIEvent): void {1733for (const index of indexes) {1734if (index < 0 || index >= this.length) {1735throw new ListError(this.user, `Invalid index ${index}`);1736}1737}17381739this.focus.set(indexes, browserEvent);1740}17411742focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1743if (this.length === 0) { return; }17441745const focus = this.focus.get();1746const index = this.findNextIndex(focus.length > 0 ? focus[0] + n : 0, loop, filter);17471748if (index > -1) {1749this.setFocus([index], browserEvent);1750}1751}17521753focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1754if (this.length === 0) { return; }17551756const focus = this.focus.get();1757const index = this.findPreviousIndex(focus.length > 0 ? focus[0] - n : 0, loop, filter);17581759if (index > -1) {1760this.setFocus([index], browserEvent);1761}1762}17631764async focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): Promise<void> {1765let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight);1766lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;1767const currentlyFocusedElementIndex = this.getFocus()[0];17681769if (currentlyFocusedElementIndex !== lastPageIndex && (currentlyFocusedElementIndex === undefined || lastPageIndex > currentlyFocusedElementIndex)) {1770const lastGoodPageIndex = this.findPreviousIndex(lastPageIndex, false, filter);17711772if (lastGoodPageIndex > -1 && currentlyFocusedElementIndex !== lastGoodPageIndex) {1773this.setFocus([lastGoodPageIndex], browserEvent);1774} else {1775this.setFocus([lastPageIndex], browserEvent);1776}1777} else {1778const previousScrollTop = this.view.getScrollTop();1779let nextpageScrollTop = previousScrollTop + this.view.renderHeight;1780if (lastPageIndex > currentlyFocusedElementIndex) {1781// scroll last page element to the top only if the last page element is below the focused element1782nextpageScrollTop -= this.view.elementHeight(lastPageIndex);1783}17841785this.view.setScrollTop(nextpageScrollTop);17861787if (this.view.getScrollTop() !== previousScrollTop) {1788this.setFocus([]);17891790// Let the scroll event listener run1791await timeout(0);1792await this.focusNextPage(browserEvent, filter);1793}1794}1795}17961797async focusPreviousPage(browserEvent?: UIEvent, filter?: (element: T) => boolean, getPaddingTop: () => number = () => 0): Promise<void> {1798let firstPageIndex: number;1799const paddingTop = getPaddingTop();1800const scrollTop = this.view.getScrollTop() + paddingTop;18011802if (scrollTop === 0) {1803firstPageIndex = this.view.indexAt(scrollTop);1804} else {1805firstPageIndex = this.view.indexAfter(scrollTop - 1);1806}18071808const currentlyFocusedElementIndex = this.getFocus()[0];18091810if (currentlyFocusedElementIndex !== firstPageIndex && (currentlyFocusedElementIndex === undefined || currentlyFocusedElementIndex >= firstPageIndex)) {1811const firstGoodPageIndex = this.findNextIndex(firstPageIndex, false, filter);18121813if (firstGoodPageIndex > -1 && currentlyFocusedElementIndex !== firstGoodPageIndex) {1814this.setFocus([firstGoodPageIndex], browserEvent);1815} else {1816this.setFocus([firstPageIndex], browserEvent);1817}1818} else {1819const previousScrollTop = scrollTop;1820this.view.setScrollTop(scrollTop - this.view.renderHeight - paddingTop);18211822if (this.view.getScrollTop() + getPaddingTop() !== previousScrollTop) {1823this.setFocus([]);18241825// Let the scroll event listener run1826await timeout(0);1827await this.focusPreviousPage(browserEvent, filter, getPaddingTop);1828}1829}1830}18311832focusLast(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1833if (this.length === 0) { return; }18341835const index = this.findPreviousIndex(this.length - 1, false, filter);18361837if (index > -1) {1838this.setFocus([index], browserEvent);1839}1840}18411842focusFirst(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1843this.focusNth(0, browserEvent, filter);1844}18451846focusNth(n: number, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {1847if (this.length === 0) { return; }18481849const index = this.findNextIndex(n, false, filter);18501851if (index > -1) {1852this.setFocus([index], browserEvent);1853}1854}18551856private findNextIndex(index: number, loop = false, filter?: (element: T) => boolean): number {1857for (let i = 0; i < this.length; i++) {1858if (index >= this.length && !loop) {1859return -1;1860}18611862index = index % this.length;18631864if (!filter || filter(this.element(index))) {1865return index;1866}18671868index++;1869}18701871return -1;1872}18731874private findPreviousIndex(index: number, loop = false, filter?: (element: T) => boolean): number {1875for (let i = 0; i < this.length; i++) {1876if (index < 0 && !loop) {1877return -1;1878}18791880index = (this.length + (index % this.length)) % this.length;18811882if (!filter || filter(this.element(index))) {1883return index;1884}18851886index--;1887}18881889return -1;1890}18911892getFocus(): number[] {1893return this.focus.get();1894}18951896getFocusedElements(): T[] {1897return this.getFocus().map(i => this.view.element(i));1898}18991900reveal(index: number, relativeTop?: number, paddingTop: number = 0): void {1901if (index < 0 || index >= this.length) {1902throw new ListError(this.user, `Invalid index ${index}`);1903}19041905const scrollTop = this.view.getScrollTop();1906const elementTop = this.view.elementTop(index);1907const elementHeight = this.view.elementHeight(index);19081909if (isNumber(relativeTop)) {1910// y = mx + b1911const m = elementHeight - this.view.renderHeight + paddingTop;1912this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop - paddingTop);1913} else {1914const viewItemBottom = elementTop + elementHeight;1915const scrollBottom = scrollTop + this.view.renderHeight;19161917if (elementTop < scrollTop + paddingTop && viewItemBottom >= scrollBottom) {1918// The element is already overflowing the viewport, no-op1919} else if (elementTop < scrollTop + paddingTop || (viewItemBottom >= scrollBottom && elementHeight >= this.view.renderHeight)) {1920this.view.setScrollTop(elementTop - paddingTop);1921} else if (viewItemBottom >= scrollBottom) {1922this.view.setScrollTop(viewItemBottom - this.view.renderHeight);1923}1924}1925}19261927/**1928* Returns the relative position of an element rendered in the list.1929* Returns `null` if the element isn't *entirely* in the visible viewport.1930*/1931getRelativeTop(index: number, paddingTop: number = 0): number | null {1932if (index < 0 || index >= this.length) {1933throw new ListError(this.user, `Invalid index ${index}`);1934}19351936const scrollTop = this.view.getScrollTop();1937const elementTop = this.view.elementTop(index);1938const elementHeight = this.view.elementHeight(index);19391940if (elementTop < scrollTop + paddingTop || elementTop + elementHeight > scrollTop + this.view.renderHeight) {1941return null;1942}19431944// y = mx + b1945const m = elementHeight - this.view.renderHeight + paddingTop;1946return Math.abs((scrollTop + paddingTop - elementTop) / m);1947}19481949isDOMFocused(): boolean {1950return isActiveElement(this.view.domNode);1951}19521953getHTMLElement(): HTMLElement {1954return this.view.domNode;1955}19561957getScrollableElement(): HTMLElement {1958return this.view.scrollableElementDomNode;1959}19601961getElementID(index: number): string {1962return this.view.getElementDomId(index);1963}19641965getElementTop(index: number): number {1966return this.view.elementTop(index);1967}19681969style(styles: IListStyles): void {1970this.styleController.style(styles);1971}19721973delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {1974this.view.delegateScrollFromMouseWheelEvent(browserEvent);1975}19761977private toListEvent({ indexes, browserEvent }: ITraitChangeEvent) {1978return { indexes, elements: indexes.map(i => this.view.element(i)), browserEvent };1979}19801981private _onFocusChange(): void {1982const focus = this.focus.get();1983this.view.domNode.classList.toggle('element-focused', focus.length > 0);1984this.onDidChangeActiveDescendant();1985}19861987private onDidChangeActiveDescendant(): void {1988const focus = this.focus.get();19891990if (focus.length > 0) {1991let id: string | undefined;19921993if (this.accessibilityProvider?.getActiveDescendantId) {1994id = this.accessibilityProvider.getActiveDescendantId(this.view.element(focus[0]));1995}19961997this.view.domNode.setAttribute('aria-activedescendant', id || this.view.getElementDomId(focus[0]));1998} else {1999this.view.domNode.removeAttribute('aria-activedescendant');2000}2001}20022003private _onSelectionChange(): void {2004const selection = this.selection.get();20052006this.view.domNode.classList.toggle('selection-none', selection.length === 0);2007this.view.domNode.classList.toggle('selection-single', selection.length === 1);2008this.view.domNode.classList.toggle('selection-multiple', selection.length > 1);2009}20102011dispose(): void {2012this._onDidDispose.fire();2013this.disposables.dispose();20142015this._onDidDispose.dispose();2016}2017}201820192020