UNPKG

monaco-editor

Version:
1,074 lines (1,073 loc) • 49.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { DataTransfers } from '../../dnd.js'; import { $, addDisposableListener, animate, getContentHeight, getContentWidth, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; import { DomEmitter } from '../../event.js'; import { EventType as TouchEventType, Gesture } from '../../touch.js'; import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js'; import { distinct, equals } from '../../../common/arrays.js'; import { Delayer, disposableTimeout } from '../../../common/async.js'; import { memoize } from '../../../common/decorators.js'; import { Emitter, Event } from '../../../common/event.js'; import { Disposable, DisposableStore, toDisposable } from '../../../common/lifecycle.js'; import { Range } from '../../../common/range.js'; import { Scrollable } from '../../../common/scrollable.js'; import { RangeMap, shift } from './rangeMap.js'; import { RowCache } from './rowCache.js'; import { BugIndicatingError } from '../../../common/errors.js'; import { clamp } from '../../../common/numbers.js'; const StaticDND = { CurrentDragAndDropData: undefined }; const DefaultOptions = { useShadows: true, verticalScrollMode: 1 /* ScrollbarVisibility.Auto */, setRowLineHeight: true, setRowHeight: true, supportDynamicHeights: false, dnd: { getDragElements(e) { return [e]; }, getDragURI() { return null; }, onDragStart() { }, onDragOver() { return false; }, drop() { }, dispose() { } }, horizontalScrolling: false, transformOptimization: true, alwaysConsumeMouseWheel: true, }; export class ElementsDragAndDropData { constructor(elements) { this.elements = elements; } update() { } getData() { return this.elements; } } export class ExternalElementsDragAndDropData { constructor(elements) { this.elements = elements; } update() { } getData() { return this.elements; } } export class NativeDragAndDropData { constructor() { this.types = []; this.files = []; } update(dataTransfer) { if (dataTransfer.types) { this.types.splice(0, this.types.length, ...dataTransfer.types); } if (dataTransfer.files) { this.files.splice(0, this.files.length); for (let i = 0; i < dataTransfer.files.length; i++) { const file = dataTransfer.files.item(i); if (file && (file.size || file.type)) { this.files.push(file); } } } } getData() { return { types: this.types, files: this.files }; } } function equalsDragFeedback(f1, f2) { if (Array.isArray(f1) && Array.isArray(f2)) { return equals(f1, f2); } return f1 === f2; } class ListViewAccessibilityProvider { constructor(accessibilityProvider) { if (accessibilityProvider?.getSetSize) { this.getSetSize = accessibilityProvider.getSetSize.bind(accessibilityProvider); } else { this.getSetSize = (e, i, l) => l; } if (accessibilityProvider?.getPosInSet) { this.getPosInSet = accessibilityProvider.getPosInSet.bind(accessibilityProvider); } else { this.getPosInSet = (e, i) => i + 1; } if (accessibilityProvider?.getRole) { this.getRole = accessibilityProvider.getRole.bind(accessibilityProvider); } else { this.getRole = _ => 'listitem'; } if (accessibilityProvider?.isChecked) { this.isChecked = accessibilityProvider.isChecked.bind(accessibilityProvider); } else { this.isChecked = _ => undefined; } } } /** * The {@link ListView} is a virtual scrolling engine. * * Given that it only renders elements within its viewport, it can hold large * collections of elements and stay very performant. The performance bottleneck * usually lies within the user's rendering code for each element. * * @remarks It is a low-level widget, not meant to be used directly. Refer to the * List widget instead. */ export class ListView { static { this.InstanceCount = 0; } get contentHeight() { return this.rangeMap.size; } get onDidScroll() { return this.scrollableElement.onScroll; } get scrollableElementDomNode() { return this.scrollableElement.getDomNode(); } get horizontalScrolling() { return this._horizontalScrolling; } set horizontalScrolling(value) { if (value === this._horizontalScrolling) { return; } if (value && this.supportDynamicHeights) { throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously'); } this._horizontalScrolling = value; this.domNode.classList.toggle('horizontal-scrolling', this._horizontalScrolling); if (this._horizontalScrolling) { for (const item of this.items) { this.measureItemWidth(item); } this.updateScrollWidth(); this.scrollableElement.setScrollDimensions({ width: getContentWidth(this.domNode) }); this.rowsContainer.style.width = `${Math.max(this.scrollWidth || 0, this.renderWidth)}px`; } else { this.scrollableElementWidthDelayer.cancel(); this.scrollableElement.setScrollDimensions({ width: this.renderWidth, scrollWidth: this.renderWidth }); this.rowsContainer.style.width = ''; } } constructor(container, virtualDelegate, renderers, options = DefaultOptions) { this.virtualDelegate = virtualDelegate; this.domId = `list_id_${++ListView.InstanceCount}`; this.renderers = new Map(); this.renderWidth = 0; this._scrollHeight = 0; this.scrollableElementUpdateDisposable = null; this.scrollableElementWidthDelayer = new Delayer(50); this.splicing = false; this.dragOverAnimationStopDisposable = Disposable.None; this.dragOverMouseY = 0; this.canDrop = false; this.currentDragFeedbackDisposable = Disposable.None; this.onDragLeaveTimeout = Disposable.None; this.disposables = new DisposableStore(); this._onDidChangeContentHeight = new Emitter(); this._onDidChangeContentWidth = new Emitter(); this.onDidChangeContentHeight = Event.latch(this._onDidChangeContentHeight.event, undefined, this.disposables); this._horizontalScrolling = false; if (options.horizontalScrolling && options.supportDynamicHeights) { throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously'); } this.items = []; this.itemId = 0; this.rangeMap = this.createRangeMap(options.paddingTop ?? 0); for (const renderer of renderers) { this.renderers.set(renderer.templateId, renderer); } this.cache = this.disposables.add(new RowCache(this.renderers)); this.lastRenderTop = 0; this.lastRenderHeight = 0; this.domNode = document.createElement('div'); this.domNode.className = 'monaco-list'; this.domNode.classList.add(this.domId); this.domNode.tabIndex = 0; this.domNode.classList.toggle('mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true); this._horizontalScrolling = options.horizontalScrolling ?? DefaultOptions.horizontalScrolling; this.domNode.classList.toggle('horizontal-scrolling', this._horizontalScrolling); this.paddingBottom = typeof options.paddingBottom === 'undefined' ? 0 : options.paddingBottom; this.accessibilityProvider = new ListViewAccessibilityProvider(options.accessibilityProvider); this.rowsContainer = document.createElement('div'); this.rowsContainer.className = 'monaco-list-rows'; const transformOptimization = options.transformOptimization ?? DefaultOptions.transformOptimization; if (transformOptimization) { this.rowsContainer.style.transform = 'translate3d(0px, 0px, 0px)'; this.rowsContainer.style.overflow = 'hidden'; this.rowsContainer.style.contain = 'strict'; } this.disposables.add(Gesture.addTarget(this.rowsContainer)); this.scrollable = this.disposables.add(new Scrollable({ forceIntegerValues: true, smoothScrollDuration: (options.smoothScrolling ?? false) ? 125 : 0, scheduleAtNextAnimationFrame: cb => scheduleAtNextAnimationFrame(getWindow(this.domNode), cb) })); this.scrollableElement = this.disposables.add(new SmoothScrollableElement(this.rowsContainer, { alwaysConsumeMouseWheel: options.alwaysConsumeMouseWheel ?? DefaultOptions.alwaysConsumeMouseWheel, horizontal: 1 /* ScrollbarVisibility.Auto */, vertical: options.verticalScrollMode ?? DefaultOptions.verticalScrollMode, useShadows: options.useShadows ?? DefaultOptions.useShadows, mouseWheelScrollSensitivity: options.mouseWheelScrollSensitivity, fastScrollSensitivity: options.fastScrollSensitivity, scrollByPage: options.scrollByPage }, this.scrollable)); this.domNode.appendChild(this.scrollableElement.getDomNode()); container.appendChild(this.domNode); this.scrollableElement.onScroll(this.onScroll, this, this.disposables); this.disposables.add(addDisposableListener(this.rowsContainer, TouchEventType.Change, e => this.onTouchChange(e))); // Prevent the monaco-scrollable-element from scrolling // https://github.com/microsoft/vscode/issues/44181 this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => e.target.scrollTop = 0)); this.disposables.add(addDisposableListener(this.domNode, 'dragover', e => this.onDragOver(this.toDragEvent(e)))); this.disposables.add(addDisposableListener(this.domNode, 'drop', e => this.onDrop(this.toDragEvent(e)))); this.disposables.add(addDisposableListener(this.domNode, 'dragleave', e => this.onDragLeave(this.toDragEvent(e)))); this.disposables.add(addDisposableListener(this.domNode, 'dragend', e => this.onDragEnd(e))); this.setRowLineHeight = options.setRowLineHeight ?? DefaultOptions.setRowLineHeight; this.setRowHeight = options.setRowHeight ?? DefaultOptions.setRowHeight; this.supportDynamicHeights = options.supportDynamicHeights ?? DefaultOptions.supportDynamicHeights; this.dnd = options.dnd ?? this.disposables.add(DefaultOptions.dnd); this.layout(options.initialSize?.height, options.initialSize?.width); } updateOptions(options) { if (options.paddingBottom !== undefined) { this.paddingBottom = options.paddingBottom; this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight }); } if (options.smoothScrolling !== undefined) { this.scrollable.setSmoothScrollDuration(options.smoothScrolling ? 125 : 0); } if (options.horizontalScrolling !== undefined) { this.horizontalScrolling = options.horizontalScrolling; } let scrollableOptions; if (options.scrollByPage !== undefined) { scrollableOptions = { ...(scrollableOptions ?? {}), scrollByPage: options.scrollByPage }; } if (options.mouseWheelScrollSensitivity !== undefined) { scrollableOptions = { ...(scrollableOptions ?? {}), mouseWheelScrollSensitivity: options.mouseWheelScrollSensitivity }; } if (options.fastScrollSensitivity !== undefined) { scrollableOptions = { ...(scrollableOptions ?? {}), fastScrollSensitivity: options.fastScrollSensitivity }; } if (scrollableOptions) { this.scrollableElement.updateOptions(scrollableOptions); } if (options.paddingTop !== undefined && options.paddingTop !== this.rangeMap.paddingTop) { // trigger a rerender const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); const offset = options.paddingTop - this.rangeMap.paddingTop; this.rangeMap.paddingTop = options.paddingTop; this.render(lastRenderRange, Math.max(0, this.lastRenderTop + offset), this.lastRenderHeight, undefined, undefined, true); this.setScrollTop(this.lastRenderTop); this.eventuallyUpdateScrollDimensions(); if (this.supportDynamicHeights) { this._rerender(this.lastRenderTop, this.lastRenderHeight); } } } createRangeMap(paddingTop) { return new RangeMap(paddingTop); } splice(start, deleteCount, elements = []) { if (this.splicing) { throw new Error('Can\'t run recursive splices.'); } this.splicing = true; try { return this._splice(start, deleteCount, elements); } finally { this.splicing = false; this._onDidChangeContentHeight.fire(this.contentHeight); } } _splice(start, deleteCount, elements = []) { const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); const deleteRange = { start, end: start + deleteCount }; const removeRange = Range.intersect(previousRenderRange, deleteRange); // try to reuse rows, avoid removing them from DOM const rowsToDispose = new Map(); for (let i = removeRange.end - 1; i >= removeRange.start; i--) { const item = this.items[i]; item.dragStartDisposable.dispose(); item.checkedDisposable.dispose(); if (item.row) { let rows = rowsToDispose.get(item.templateId); if (!rows) { rows = []; rowsToDispose.set(item.templateId, rows); } const renderer = this.renderers.get(item.templateId); if (renderer && renderer.disposeElement) { renderer.disposeElement(item.element, i, item.row.templateData, item.size); } rows.unshift(item.row); } item.row = null; item.stale = true; } const previousRestRange = { start: start + deleteCount, end: this.items.length }; const previousRenderedRestRange = Range.intersect(previousRestRange, previousRenderRange); const previousUnrenderedRestRanges = Range.relativeComplement(previousRestRange, previousRenderRange); const inserted = elements.map(element => ({ id: String(this.itemId++), element, templateId: this.virtualDelegate.getTemplateId(element), size: this.virtualDelegate.getHeight(element), width: undefined, hasDynamicHeight: !!this.virtualDelegate.hasDynamicHeight && this.virtualDelegate.hasDynamicHeight(element), lastDynamicHeightWidth: undefined, row: null, uri: undefined, dropTarget: false, dragStartDisposable: Disposable.None, checkedDisposable: Disposable.None, stale: false })); let deleted; // TODO@joao: improve this optimization to catch even more cases if (start === 0 && deleteCount >= this.items.length) { this.rangeMap = this.createRangeMap(this.rangeMap.paddingTop); this.rangeMap.splice(0, 0, inserted); deleted = this.items; this.items = inserted; } else { this.rangeMap.splice(start, deleteCount, inserted); deleted = this.items.splice(start, deleteCount, ...inserted); } const delta = elements.length - deleteCount; const renderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); const renderedRestRange = shift(previousRenderedRestRange, delta); const updateRange = Range.intersect(renderRange, renderedRestRange); for (let i = updateRange.start; i < updateRange.end; i++) { this.updateItemInDOM(this.items[i], i); } const removeRanges = Range.relativeComplement(renderedRestRange, renderRange); for (const range of removeRanges) { for (let i = range.start; i < range.end; i++) { this.removeItemFromDOM(i); } } const unrenderedRestRanges = previousUnrenderedRestRanges.map(r => shift(r, delta)); const elementsRange = { start, end: start + elements.length }; const insertRanges = [elementsRange, ...unrenderedRestRanges].map(r => Range.intersect(renderRange, r)).reverse(); for (const range of insertRanges) { for (let i = range.end - 1; i >= range.start; i--) { const item = this.items[i]; const rows = rowsToDispose.get(item.templateId); const row = rows?.pop(); this.insertItemInDOM(i, row); } } for (const rows of rowsToDispose.values()) { for (const row of rows) { this.cache.release(row); } } this.eventuallyUpdateScrollDimensions(); if (this.supportDynamicHeights) { this._rerender(this.scrollTop, this.renderHeight); } return deleted.map(i => i.element); } eventuallyUpdateScrollDimensions() { this._scrollHeight = this.contentHeight; this.rowsContainer.style.height = `${this._scrollHeight}px`; if (!this.scrollableElementUpdateDisposable) { this.scrollableElementUpdateDisposable = scheduleAtNextAnimationFrame(getWindow(this.domNode), () => { this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight }); this.updateScrollWidth(); this.scrollableElementUpdateDisposable = null; }); } } eventuallyUpdateScrollWidth() { if (!this.horizontalScrolling) { this.scrollableElementWidthDelayer.cancel(); return; } this.scrollableElementWidthDelayer.trigger(() => this.updateScrollWidth()); } updateScrollWidth() { if (!this.horizontalScrolling) { return; } let scrollWidth = 0; for (const item of this.items) { if (typeof item.width !== 'undefined') { scrollWidth = Math.max(scrollWidth, item.width); } } this.scrollWidth = scrollWidth; this.scrollableElement.setScrollDimensions({ scrollWidth: scrollWidth === 0 ? 0 : (scrollWidth + 10) }); this._onDidChangeContentWidth.fire(this.scrollWidth); } rerender() { if (!this.supportDynamicHeights) { return; } for (const item of this.items) { item.lastDynamicHeightWidth = undefined; } this._rerender(this.lastRenderTop, this.lastRenderHeight); } get length() { return this.items.length; } get renderHeight() { const scrollDimensions = this.scrollableElement.getScrollDimensions(); return scrollDimensions.height; } get firstVisibleIndex() { const range = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); return range.start; } element(index) { return this.items[index].element; } indexOf(element) { return this.items.findIndex(item => item.element === element); } domElement(index) { const row = this.items[index].row; return row && row.domNode; } elementHeight(index) { return this.items[index].size; } elementTop(index) { return this.rangeMap.positionAt(index); } indexAt(position) { return this.rangeMap.indexAt(position); } indexAfter(position) { return this.rangeMap.indexAfter(position); } layout(height, width) { const scrollDimensions = { height: typeof height === 'number' ? height : getContentHeight(this.domNode) }; if (this.scrollableElementUpdateDisposable) { this.scrollableElementUpdateDisposable.dispose(); this.scrollableElementUpdateDisposable = null; scrollDimensions.scrollHeight = this.scrollHeight; } this.scrollableElement.setScrollDimensions(scrollDimensions); if (typeof width !== 'undefined') { this.renderWidth = width; if (this.supportDynamicHeights) { this._rerender(this.scrollTop, this.renderHeight); } } if (this.horizontalScrolling) { this.scrollableElement.setScrollDimensions({ width: typeof width === 'number' ? width : getContentWidth(this.domNode) }); } } // Render render(previousRenderRange, renderTop, renderHeight, renderLeft, scrollWidth, updateItemsInDOM = false) { const renderRange = this.getRenderRange(renderTop, renderHeight); const rangesToInsert = Range.relativeComplement(renderRange, previousRenderRange).reverse(); const rangesToRemove = Range.relativeComplement(previousRenderRange, renderRange); if (updateItemsInDOM) { const rangesToUpdate = Range.intersect(previousRenderRange, renderRange); for (let i = rangesToUpdate.start; i < rangesToUpdate.end; i++) { this.updateItemInDOM(this.items[i], i); } } this.cache.transact(() => { for (const range of rangesToRemove) { for (let i = range.start; i < range.end; i++) { this.removeItemFromDOM(i); } } for (const range of rangesToInsert) { for (let i = range.end - 1; i >= range.start; i--) { this.insertItemInDOM(i); } } }); if (renderLeft !== undefined) { this.rowsContainer.style.left = `-${renderLeft}px`; } this.rowsContainer.style.top = `-${renderTop}px`; if (this.horizontalScrolling && scrollWidth !== undefined) { this.rowsContainer.style.width = `${Math.max(scrollWidth, this.renderWidth)}px`; } this.lastRenderTop = renderTop; this.lastRenderHeight = renderHeight; } // DOM operations insertItemInDOM(index, row) { const item = this.items[index]; if (!item.row) { if (row) { item.row = row; item.stale = true; } else { const result = this.cache.alloc(item.templateId); item.row = result.row; item.stale ||= result.isReusingConnectedDomNode; } } const role = this.accessibilityProvider.getRole(item.element) || 'listitem'; item.row.domNode.setAttribute('role', role); const checked = this.accessibilityProvider.isChecked(item.element); if (typeof checked === 'boolean') { item.row.domNode.setAttribute('aria-checked', String(!!checked)); } else if (checked) { const update = (checked) => item.row.domNode.setAttribute('aria-checked', String(!!checked)); update(checked.value); item.checkedDisposable = checked.onDidChange(() => update(checked.value)); } if (item.stale || !item.row.domNode.parentElement) { const referenceNode = this.items.at(index + 1)?.row?.domNode ?? null; if (item.row.domNode.parentElement !== this.rowsContainer || item.row.domNode.nextElementSibling !== referenceNode) { this.rowsContainer.insertBefore(item.row.domNode, referenceNode); } item.stale = false; } this.updateItemInDOM(item, index); const renderer = this.renderers.get(item.templateId); if (!renderer) { throw new Error(`No renderer found for template id ${item.templateId}`); } renderer?.renderElement(item.element, index, item.row.templateData, item.size); const uri = this.dnd.getDragURI(item.element); item.dragStartDisposable.dispose(); item.row.domNode.draggable = !!uri; if (uri) { item.dragStartDisposable = addDisposableListener(item.row.domNode, 'dragstart', event => this.onDragStart(item.element, uri, event)); } if (this.horizontalScrolling) { this.measureItemWidth(item); this.eventuallyUpdateScrollWidth(); } } measureItemWidth(item) { if (!item.row || !item.row.domNode) { return; } item.row.domNode.style.width = 'fit-content'; item.width = getContentWidth(item.row.domNode); const style = getWindow(item.row.domNode).getComputedStyle(item.row.domNode); if (style.paddingLeft) { item.width += parseFloat(style.paddingLeft); } if (style.paddingRight) { item.width += parseFloat(style.paddingRight); } item.row.domNode.style.width = ''; } updateItemInDOM(item, index) { item.row.domNode.style.top = `${this.elementTop(index)}px`; if (this.setRowHeight) { item.row.domNode.style.height = `${item.size}px`; } if (this.setRowLineHeight) { item.row.domNode.style.lineHeight = `${item.size}px`; } item.row.domNode.setAttribute('data-index', `${index}`); item.row.domNode.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false'); item.row.domNode.setAttribute('data-parity', index % 2 === 0 ? 'even' : 'odd'); item.row.domNode.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length))); item.row.domNode.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index))); item.row.domNode.setAttribute('id', this.getElementDomId(index)); item.row.domNode.classList.toggle('drop-target', item.dropTarget); } removeItemFromDOM(index) { const item = this.items[index]; item.dragStartDisposable.dispose(); item.checkedDisposable.dispose(); if (item.row) { const renderer = this.renderers.get(item.templateId); if (renderer && renderer.disposeElement) { renderer.disposeElement(item.element, index, item.row.templateData, item.size); } this.cache.release(item.row); item.row = null; } if (this.horizontalScrolling) { this.eventuallyUpdateScrollWidth(); } } getScrollTop() { const scrollPosition = this.scrollableElement.getScrollPosition(); return scrollPosition.scrollTop; } setScrollTop(scrollTop, reuseAnimation) { if (this.scrollableElementUpdateDisposable) { this.scrollableElementUpdateDisposable.dispose(); this.scrollableElementUpdateDisposable = null; this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight }); } this.scrollableElement.setScrollPosition({ scrollTop, reuseAnimation }); } get scrollTop() { return this.getScrollTop(); } set scrollTop(scrollTop) { this.setScrollTop(scrollTop); } get scrollHeight() { return this._scrollHeight + (this.horizontalScrolling ? 10 : 0) + this.paddingBottom; } // Events get onMouseClick() { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'click')).event, e => this.toMouseEvent(e), this.disposables); } get onMouseDblClick() { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'dblclick')).event, e => this.toMouseEvent(e), this.disposables); } get onMouseMiddleClick() { return Event.filter(Event.map(this.disposables.add(new DomEmitter(this.domNode, 'auxclick')).event, e => this.toMouseEvent(e), this.disposables), e => e.browserEvent.button === 1, this.disposables); } get onMouseDown() { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mousedown')).event, e => this.toMouseEvent(e), this.disposables); } get onMouseOver() { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mouseover')).event, e => this.toMouseEvent(e), this.disposables); } get onMouseOut() { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mouseout')).event, e => this.toMouseEvent(e), this.disposables); } get onContextMenu() { return Event.any(Event.map(this.disposables.add(new DomEmitter(this.domNode, 'contextmenu')).event, e => this.toMouseEvent(e), this.disposables), Event.map(this.disposables.add(new DomEmitter(this.domNode, TouchEventType.Contextmenu)).event, e => this.toGestureEvent(e), this.disposables)); } get onTouchStart() { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'touchstart')).event, e => this.toTouchEvent(e), this.disposables); } get onTap() { return Event.map(this.disposables.add(new DomEmitter(this.rowsContainer, TouchEventType.Tap)).event, e => this.toGestureEvent(e), this.disposables); } toMouseEvent(browserEvent) { const index = this.getItemIndexFromEventTarget(browserEvent.target || null); const item = typeof index === 'undefined' ? undefined : this.items[index]; const element = item && item.element; return { browserEvent, index, element }; } toTouchEvent(browserEvent) { const index = this.getItemIndexFromEventTarget(browserEvent.target || null); const item = typeof index === 'undefined' ? undefined : this.items[index]; const element = item && item.element; return { browserEvent, index, element }; } toGestureEvent(browserEvent) { const index = this.getItemIndexFromEventTarget(browserEvent.initialTarget || null); const item = typeof index === 'undefined' ? undefined : this.items[index]; const element = item && item.element; return { browserEvent, index, element }; } toDragEvent(browserEvent) { const index = this.getItemIndexFromEventTarget(browserEvent.target || null); const item = typeof index === 'undefined' ? undefined : this.items[index]; const element = item && item.element; const sector = this.getTargetSector(browserEvent, index); return { browserEvent, index, element, sector }; } onScroll(e) { try { const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); this.render(previousRenderRange, e.scrollTop, e.height, e.scrollLeft, e.scrollWidth); if (this.supportDynamicHeights) { this._rerender(e.scrollTop, e.height, e.inSmoothScrolling); } } catch (err) { console.error('Got bad scroll event:', e); throw err; } } onTouchChange(event) { event.preventDefault(); event.stopPropagation(); this.scrollTop -= event.translationY; } // DND onDragStart(element, uri, event) { if (!event.dataTransfer) { return; } const elements = this.dnd.getDragElements(element); event.dataTransfer.effectAllowed = 'copyMove'; event.dataTransfer.setData(DataTransfers.TEXT, uri); if (event.dataTransfer.setDragImage) { let label; if (this.dnd.getDragLabel) { label = this.dnd.getDragLabel(elements, event); } if (typeof label === 'undefined') { label = String(elements.length); } const dragImage = $('.monaco-drag-image'); dragImage.textContent = label; const getDragImageContainer = (e) => { while (e && !e.classList.contains('monaco-workbench')) { e = e.parentElement; } return e || this.domNode.ownerDocument; }; const container = getDragImageContainer(this.domNode); container.appendChild(dragImage); event.dataTransfer.setDragImage(dragImage, -10, -10); setTimeout(() => dragImage.remove(), 0); } this.domNode.classList.add('dragging'); this.currentDragData = new ElementsDragAndDropData(elements); StaticDND.CurrentDragAndDropData = new ExternalElementsDragAndDropData(elements); this.dnd.onDragStart?.(this.currentDragData, event); } onDragOver(event) { event.browserEvent.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) this.onDragLeaveTimeout.dispose(); if (StaticDND.CurrentDragAndDropData && StaticDND.CurrentDragAndDropData.getData() === 'vscode-ui') { return false; } this.setupDragAndDropScrollTopAnimation(event.browserEvent); if (!event.browserEvent.dataTransfer) { return false; } // Drag over from outside if (!this.currentDragData) { if (StaticDND.CurrentDragAndDropData) { // Drag over from another list this.currentDragData = StaticDND.CurrentDragAndDropData; } else { // Drag over from the desktop if (!event.browserEvent.dataTransfer.types) { return false; } this.currentDragData = new NativeDragAndDropData(); } } const result = this.dnd.onDragOver(this.currentDragData, event.element, event.index, event.sector, event.browserEvent); this.canDrop = typeof result === 'boolean' ? result : result.accept; if (!this.canDrop) { this.currentDragFeedback = undefined; this.currentDragFeedbackDisposable.dispose(); return false; } event.browserEvent.dataTransfer.dropEffect = (typeof result !== 'boolean' && result.effect?.type === 0 /* ListDragOverEffectType.Copy */) ? 'copy' : 'move'; let feedback; if (typeof result !== 'boolean' && result.feedback) { feedback = result.feedback; } else { if (typeof event.index === 'undefined') { feedback = [-1]; } else { feedback = [event.index]; } } // sanitize feedback list feedback = distinct(feedback).filter(i => i >= -1 && i < this.length).sort((a, b) => a - b); feedback = feedback[0] === -1 ? [-1] : feedback; let dragOverEffectPosition = typeof result !== 'boolean' && result.effect && result.effect.position ? result.effect.position : "drop-target" /* ListDragOverEffectPosition.Over */; if (equalsDragFeedback(this.currentDragFeedback, feedback) && this.currentDragFeedbackPosition === dragOverEffectPosition) { return true; } this.currentDragFeedback = feedback; this.currentDragFeedbackPosition = dragOverEffectPosition; this.currentDragFeedbackDisposable.dispose(); if (feedback[0] === -1) { // entire list feedback this.domNode.classList.add(dragOverEffectPosition); this.rowsContainer.classList.add(dragOverEffectPosition); this.currentDragFeedbackDisposable = toDisposable(() => { this.domNode.classList.remove(dragOverEffectPosition); this.rowsContainer.classList.remove(dragOverEffectPosition); }); } else { if (feedback.length > 1 && dragOverEffectPosition !== "drop-target" /* ListDragOverEffectPosition.Over */) { throw new Error('Can\'t use multiple feedbacks with position different than \'over\''); } // Make sure there is no flicker when moving between two items // Always use the before feedback if possible if (dragOverEffectPosition === "drop-target-after" /* ListDragOverEffectPosition.After */) { if (feedback[0] < this.length - 1) { feedback[0] += 1; dragOverEffectPosition = "drop-target-before" /* ListDragOverEffectPosition.Before */; } } for (const index of feedback) { const item = this.items[index]; item.dropTarget = true; item.row?.domNode.classList.add(dragOverEffectPosition); } this.currentDragFeedbackDisposable = toDisposable(() => { for (const index of feedback) { const item = this.items[index]; item.dropTarget = false; item.row?.domNode.classList.remove(dragOverEffectPosition); } }); } return true; } onDragLeave(event) { this.onDragLeaveTimeout.dispose(); this.onDragLeaveTimeout = disposableTimeout(() => this.clearDragOverFeedback(), 100, this.disposables); if (this.currentDragData) { this.dnd.onDragLeave?.(this.currentDragData, event.element, event.index, event.browserEvent); } } onDrop(event) { if (!this.canDrop) { return; } const dragData = this.currentDragData; this.teardownDragAndDropScrollTopAnimation(); this.clearDragOverFeedback(); this.domNode.classList.remove('dragging'); this.currentDragData = undefined; StaticDND.CurrentDragAndDropData = undefined; if (!dragData || !event.browserEvent.dataTransfer) { return; } event.browserEvent.preventDefault(); dragData.update(event.browserEvent.dataTransfer); this.dnd.drop(dragData, event.element, event.index, event.sector, event.browserEvent); } onDragEnd(event) { this.canDrop = false; this.teardownDragAndDropScrollTopAnimation(); this.clearDragOverFeedback(); this.domNode.classList.remove('dragging'); this.currentDragData = undefined; StaticDND.CurrentDragAndDropData = undefined; this.dnd.onDragEnd?.(event); } clearDragOverFeedback() { this.currentDragFeedback = undefined; this.currentDragFeedbackPosition = undefined; this.currentDragFeedbackDisposable.dispose(); this.currentDragFeedbackDisposable = Disposable.None; } // DND scroll top animation setupDragAndDropScrollTopAnimation(event) { if (!this.dragOverAnimationDisposable) { const viewTop = getTopLeftOffset(this.domNode).top; this.dragOverAnimationDisposable = animate(getWindow(this.domNode), this.animateDragAndDropScrollTop.bind(this, viewTop)); } this.dragOverAnimationStopDisposable.dispose(); this.dragOverAnimationStopDisposable = disposableTimeout(() => { if (this.dragOverAnimationDisposable) { this.dragOverAnimationDisposable.dispose(); this.dragOverAnimationDisposable = undefined; } }, 1000, this.disposables); this.dragOverMouseY = event.pageY; } animateDragAndDropScrollTop(viewTop) { if (this.dragOverMouseY === undefined) { return; } const diff = this.dragOverMouseY - viewTop; const upperLimit = this.renderHeight - 35; if (diff < 35) { this.scrollTop += Math.max(-14, Math.floor(0.3 * (diff - 35))); } else if (diff > upperLimit) { this.scrollTop += Math.min(14, Math.floor(0.3 * (diff - upperLimit))); } } teardownDragAndDropScrollTopAnimation() { this.dragOverAnimationStopDisposable.dispose(); if (this.dragOverAnimationDisposable) { this.dragOverAnimationDisposable.dispose(); this.dragOverAnimationDisposable = undefined; } } // Util getTargetSector(browserEvent, targetIndex) { if (targetIndex === undefined) { return undefined; } const relativePosition = browserEvent.offsetY / this.items[targetIndex].size; const sector = Math.floor(relativePosition / 0.25); return clamp(sector, 0, 3); } getItemIndexFromEventTarget(target) { const scrollableElement = this.scrollableElement.getDomNode(); let element = target; while ((isHTMLElement(element) || isSVGElement(element)) && element !== this.rowsContainer && scrollableElement.contains(element)) { const rawIndex = element.getAttribute('data-index'); if (rawIndex) { const index = Number(rawIndex); if (!isNaN(index)) { return index; } } element = element.parentElement; } return undefined; } getRenderRange(renderTop, renderHeight) { return { start: this.rangeMap.indexAt(renderTop), end: this.rangeMap.indexAfter(renderTop + renderHeight - 1) }; } /** * Given a stable rendered state, checks every rendered element whether it needs * to be probed for dynamic height. Adjusts scroll height and top if necessary. */ _rerender(renderTop, renderHeight, inSmoothScrolling) { const previousRenderRange = this.getRenderRange(renderTop, renderHeight); // Let's remember the second element's position, this helps in scrolling up // and preserving a linear upwards scroll movement let anchorElementIndex; let anchorElementTopDelta; if (renderTop === this.elementTop(previousRenderRange.start)) { anchorElementIndex = previousRenderRange.start; anchorElementTopDelta = 0; } else if (previousRenderRange.end - previousRenderRange.start > 1) { anchorElementIndex = previousRenderRange.start + 1; anchorElementTopDelta = this.elementTop(anchorElementIndex) - renderTop; } let heightDiff = 0; while (true) { const renderRange = this.getRenderRange(renderTop, renderHeight); let didChange = false; for (let i = renderRange.start; i < renderRange.end; i++) { const diff = this.probeDynamicHeight(i); if (diff !== 0) { this.rangeMap.splice(i, 1, [this.items[i]]); } heightDiff += diff; didChange = didChange || diff !== 0; } if (!didChange) { if (heightDiff !== 0) { this.eventuallyUpdateScrollDimensions(); } const unrenderRanges = Range.relativeComplement(previousRenderRange, renderRange); for (const range of unrenderRanges) { for (let i = range.start; i < range.end; i++) { if (this.items[i].row) { this.removeItemFromDOM(i); } } } const renderRanges = Range.relativeComplement(renderRange, previousRenderRange).reverse(); for (const range of renderRanges) { for (let i = range.end - 1; i >= range.start; i--) { this.insertItemInDOM(i); } } for (let i = renderRange.start; i < renderRange.end; i++) { if (this.items[i].row) { this.updateItemInDOM(this.items[i], i); } } if (typeof anchorElementIndex === 'number') { // To compute a destination scroll top, we need to take into account the current smooth scrolling // animation, and then reuse it with a new target (to avoid prolonging the scroll) // See https://github.com/microsoft/vscode/issues/104144 // See https://github.com/microsoft/vscode/pull/104284 // See https://github.com/microsoft/vscode/issues/107704 const deltaScrollTop = this.scrollable.getFutureScrollPosition().scrollTop - renderTop; const newScrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta + deltaScrollTop; this.setScrollTop(newScrollTop, inSmoothScrolling); } this._onDidChangeContentHeight.fire(this.contentHeight); return; } } } probeDynamicHeight(index) { const item = this.items[index]; if (!!this.virtualDelegate.getDynamicHeight) { const newSize = this.virtualDelegate.getDynamicHeight(item.element); if (newSize !== null) { const size = item.size; item.size = newSize; item.lastDynamicHeightWidth = this.renderWidth; return newSize - size; } } if (!item.hasDynamicHeight || item.lastDynamicHeightWidth === this.renderWidth) { return 0; } if (!!this.virtualDelegate.hasDynamicHeight && !this.virtualDelegate.hasDynamicHeight(item.element)) { return 0; } const size = item.size; if (item.row) { item.row.domNode.style.height = ''; item.size = item.row.domNode.offsetHeight; if (item.size === 0 && !isAncestor(item.row.domNode, getWindow(item.row.domNode).document.body)) { console.warn('Measuring item node that is not in DOM! Add ListView to the DOM before measuring row height!', new Error().stack); } item.lastDynamicHeightWidth = this.renderWidth; return item.size - size; } const { row } = this.cache.alloc(item.templateId); row.domNode.style.height = ''; this.rowsContainer.appendChild(row.domNode); const renderer = this.renderers.get(item.templateId); if (!renderer) { throw new BugIndicatingError('Missing renderer for templateId: ' + item.templateId); } renderer.renderElement(item.element, index, row.templateData, undefined); item.size = row.domNode.offsetHeight; renderer.disposeElement?.(item.element, index, row.templateData, undefined); this.virtualDelegate.setDynamicHeight?.(item.element, item.size); item.lastDynamicHeightWidth = this.renderWidth; row.domNode.remove(); this.cache.release(row); return item.size - size; } getElementDomId(index) { return `${this.domId}_${index}`; } // Dispose dispose() { for (const item of this.items) { item.dragStartDisposable.dispose(); item.checkedDisposable.dispose(); if (item.row) { const renderer = this.renderers.get(item.row.templateId); if (renderer) { renderer.disposeElement?.(item.element, -1, item.row.templateData, undefined); renderer.disposeTemplate(item.row.templateData); } } } this.items = []; this.domNode?.remove(); this.dragOverAnimationDisposable?.dispose(); this.disposables.dispose(); } } __decorate([ memoize ], ListView.prototype, "onMouseClick", null); __decorate([ memoize ], ListView.prototype, "onMouseDblClick", null); __decorate([ memoize ], ListView.prototype, "onMouseMiddleClick", null); __decorate([ memoize ], ListView.prototype, "onMouseDown", null); __decorate([ memoize ], ListView.prototype, "onMouseOver", null); __decorate([ memoize ], ListView.prototype, "onMouseOut", null); __decorate([ memoize ], ListView.prototype, "onContextMenu", null); __decorate([ memoize ], ListView.prototype, "onTouchStart", null); __decorate([ memoize ], ListView.prototype, "onTap", null);