listView.ts 13.1 KB
Newer Older
J
Joao Moreno 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

J
Joao Moreno 已提交
6
import { getOrDefault } from 'vs/base/common/objects';
J
Joao Moreno 已提交
7
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
8
import { Gesture, EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch';
J
Joao Moreno 已提交
9
import * as DOM from 'vs/base/browser/dom';
J
Joao Moreno 已提交
10
import Event, { mapEvent, filterEvent } from 'vs/base/common/event';
11
import { domEvent } from 'vs/base/browser/event';
A
Alex Dima 已提交
12
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
J
Joao Moreno 已提交
13
import { ScrollEvent, ScrollbarVisibility } from 'vs/base/common/scrollable';
J
Joao Moreno 已提交
14
import { RangeMap, IRange, relativeComplement, intersect, shift } from './rangeMap';
J
Joao Moreno 已提交
15
import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list';
J
Joao Moreno 已提交
16
import { RowCache, IRow } from './rowCache';
17
import { isWindows } from 'vs/base/common/platform';
18
import * as browser from 'vs/base/browser/browser';
J
Joao Moreno 已提交
19
import { ISpliceable } from 'vs/base/common/sequence';
J
Joao Moreno 已提交
20
import { memoize } from 'vs/base/common/decorators';
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

function canUseTranslate3d(): boolean {
	if (browser.isFirefox) {
		return false;
	}

	if (browser.getZoomLevel() !== 0) {
		return false;
	}

	// see https://github.com/Microsoft/vscode/issues/24483
	if (browser.isChromev56) {
		const pixelRatio = browser.getPixelRatio();
		if (Math.floor(pixelRatio) !== pixelRatio) {
			// Not an integer
			return false;
		}
	}

	return true;
}

J
Joao Moreno 已提交
43

44
interface IItem<T> {
J
Joao Moreno 已提交
45
	id: string;
46 47
	element: T;
	size: number;
J
Joao Moreno 已提交
48 49
	templateId: string;
	row: IRow;
J
Joao Moreno 已提交
50 51
}

J
Joao Moreno 已提交
52 53 54 55 56 57 58 59
export interface IListViewOptions {
	useShadows?: boolean;
}

const DefaultOptions: IListViewOptions = {
	useShadows: true
};

J
Joao Moreno 已提交
60
export class ListView<T> implements ISpliceable<T>, IDisposable {
J
Joao Moreno 已提交
61

62
	private items: IItem<T>[];
J
Joao Moreno 已提交
63
	private itemId: number;
J
Joao Moreno 已提交
64
	private rangeMap: RangeMap;
J
Joao Moreno 已提交
65
	private cache: RowCache<T>;
J
Joao Moreno 已提交
66
	private renderers = new Map<string, IRenderer<T, any>>();
67 68
	private lastRenderTop: number;
	private lastRenderHeight: number;
J
Joao Moreno 已提交
69
	private _domNode: HTMLElement;
J
Joao Moreno 已提交
70 71
	private gesture: Gesture;
	private rowsContainer: HTMLElement;
A
Alex Dima 已提交
72
	private scrollableElement: ScrollableElement;
J
Joao Moreno 已提交
73
	private splicing = false;
74
	private disposables: IDisposable[];
J
Joao Moreno 已提交
75

76 77 78
	constructor(
		container: HTMLElement,
		private delegate: IDelegate<T>,
J
Joao Moreno 已提交
79 80
		renderers: IRenderer<T, any>[],
		options: IListViewOptions = DefaultOptions
81
	) {
J
Joao Moreno 已提交
82
		this.items = [];
J
Joao Moreno 已提交
83
		this.itemId = 0;
J
Joao Moreno 已提交
84
		this.rangeMap = new RangeMap();
J
Joao Moreno 已提交
85 86 87 88 89

		for (const renderer of renderers) {
			this.renderers.set(renderer.templateId, renderer);
		}

J
Joao Moreno 已提交
90
		this.cache = new RowCache(this.renderers);
J
Joao Moreno 已提交
91

92 93
		this.lastRenderTop = 0;
		this.lastRenderHeight = 0;
94

J
Joao Moreno 已提交
95 96
		this._domNode = document.createElement('div');
		this._domNode.className = 'monaco-list';
J
Joao Moreno 已提交
97

J
Joao Moreno 已提交
98 99
		this.rowsContainer = document.createElement('div');
		this.rowsContainer.className = 'monaco-list-rows';
100
		Gesture.addTarget(this.rowsContainer);
J
Joao Moreno 已提交
101

102
		this.scrollableElement = new ScrollableElement(this.rowsContainer, {
103
			alwaysConsumeMouseWheel: true,
A
Alex Dima 已提交
104 105
			horizontal: ScrollbarVisibility.Hidden,
			vertical: ScrollbarVisibility.Auto,
106
			useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows)
J
Joao Moreno 已提交
107
		});
J
Joao Moreno 已提交
108

J
Joao Moreno 已提交
109 110 111
		this._domNode.appendChild(this.scrollableElement.getDomNode());
		container.appendChild(this._domNode);

112 113 114 115
		this.disposables = [this.rangeMap, this.gesture, this.scrollableElement];

		this.scrollableElement.onScroll(this.onScroll, this, this.disposables);
		domEvent(this.rowsContainer, TouchEventType.Change)(this.onTouchChange, this, this.disposables);
116 117 118 119

		this.layout();
	}

J
Joao Moreno 已提交
120 121 122 123
	get domNode(): HTMLElement {
		return this._domNode;
	}

J
Joao Moreno 已提交
124
	splice(start: number, deleteCount: number, elements: T[] = []): T[] {
J
Joao Moreno 已提交
125 126 127 128 129 130 131 132 133 134 135 136 137 138
		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;
		}
	}

	private _splice(start: number, deleteCount: number, elements: T[] = []): T[] {
139
		const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
J
Joao Moreno 已提交
140 141
		const deleteRange = { start, end: start + deleteCount };
		const removeRange = intersect(previousRenderRange, deleteRange);
J
Joao Moreno 已提交
142

J
Joao Moreno 已提交
143
		for (let i = removeRange.start; i < removeRange.end; i++) {
J
Joao Moreno 已提交
144 145
			this.removeItemFromDOM(this.items[i]);
		}
146

J
Joao Moreno 已提交
147 148 149 150
		const previousRestRange: IRange = { start: start + deleteCount, end: this.items.length };
		const previousRenderedRestRange = intersect(previousRestRange, previousRenderRange);
		const previousUnrenderedRestRanges = relativeComplement(previousRestRange, previousRenderRange);

151
		const inserted = elements.map<IItem<T>>(element => ({
J
Joao Moreno 已提交
152
			id: String(this.itemId++),
153 154 155 156 157 158 159
			element,
			size: this.delegate.getHeight(element),
			templateId: this.delegate.getTemplateId(element),
			row: null
		}));

		this.rangeMap.splice(start, deleteCount, ...inserted);
160
		const deleted = this.items.splice(start, deleteCount, ...inserted);
J
Joao Moreno 已提交
161 162

		const delta = elements.length - deleteCount;
163
		const renderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
J
Joao Moreno 已提交
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
		const renderedRestRange = shift(previousRenderedRestRange, delta);
		const updateRange = intersect(renderRange, renderedRestRange);

		for (let i = updateRange.start; i < updateRange.end; i++) {
			this.updateItemInDOM(this.items[i], i);
		}

		const removeRanges = relativeComplement(renderedRestRange, renderRange);

		for (let r = 0; r < removeRanges.length; r++) {
			const removeRange = removeRanges[r];

			for (let i = removeRange.start; i < removeRange.end; i++) {
				this.removeItemFromDOM(this.items[i]);
			}
		}

		const unrenderedRestRanges = previousUnrenderedRestRanges.map(r => shift(r, delta));
		const elementsRange = { start, end: start + elements.length };
		const insertRanges = [elementsRange, ...unrenderedRestRanges].map(r => intersect(renderRange, r));

		for (let r = 0; r < insertRanges.length; r++) {
			const insertRange = insertRanges[r];
J
Joao Moreno 已提交
187

J
Joao Moreno 已提交
188 189 190
			for (let i = insertRange.start; i < insertRange.end; i++) {
				this.insertItemInDOM(this.items[i], i);
			}
J
Joao Moreno 已提交
191
		}
192

J
Joao Moreno 已提交
193
		const scrollHeight = this.getContentHeight();
J
Johannes Rieken 已提交
194
		this.rowsContainer.style.height = `${scrollHeight}px`;
195
		this.scrollableElement.setScrollDimensions({ scrollHeight });
J
Joao Moreno 已提交
196 197

		return deleted.map(i => i.element);
J
Joao Moreno 已提交
198 199
	}

J
Joao Moreno 已提交
200 201 202 203
	get length(): number {
		return this.items.length;
	}

J
Joao Moreno 已提交
204
	get renderHeight(): number {
205 206
		const scrollDimensions = this.scrollableElement.getScrollDimensions();
		return scrollDimensions.height;
J
Joao Moreno 已提交
207 208
	}

J
Joao Moreno 已提交
209 210 211 212
	element(index: number): T {
		return this.items[index].element;
	}

213 214 215 216 217
	domElement(index: number): HTMLElement {
		const row = this.items[index].row;
		return row && row.domNode;
	}

J
Joao Moreno 已提交
218 219 220 221 222 223 224 225
	elementHeight(index: number): number {
		return this.items[index].size;
	}

	elementTop(index: number): number {
		return this.rangeMap.positionAt(index);
	}

J
Joao Moreno 已提交
226 227 228 229 230 231 232 233
	indexAt(position: number): number {
		return this.rangeMap.indexAt(position);
	}

	indexAfter(position: number): number {
		return this.rangeMap.indexAfter(position);
	}

J
Joao Moreno 已提交
234
	layout(height?: number): void {
235
		this.scrollableElement.setScrollDimensions({
236 237
			height: height || DOM.getContentHeight(this._domNode)
		});
J
Joao Moreno 已提交
238 239 240 241
	}

	// Render

J
Joao Moreno 已提交
242
	private render(renderTop: number, renderHeight: number): void {
243 244
		const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
		const renderRange = this.getRenderRange(renderTop, renderHeight);
J
Joao Moreno 已提交
245

246 247
		const rangesToInsert = relativeComplement(renderRange, previousRenderRange);
		const rangesToRemove = relativeComplement(previousRenderRange, renderRange);
J
Joao Moreno 已提交
248

J
Joao Moreno 已提交
249 250 251 252 253 254 255 256 257 258 259
		for (const range of rangesToInsert) {
			for (let i = range.start; i < range.end; i++) {
				this.insertItemInDOM(this.items[i], i);
			}
		}

		for (const range of rangesToRemove) {
			for (let i = range.start; i < range.end; i++) {
				this.removeItemFromDOM(this.items[i], );
			}
		}
J
Joao Moreno 已提交
260

261 262 263 264 265 266 267 268
		if (canUseTranslate3d() && !isWindows /* Windows: translate3d breaks subpixel-antialias (ClearType) unless a background is defined */) {
			const transform = `translate3d(0px, -${renderTop}px, 0px)`;
			this.rowsContainer.style.transform = transform;
			this.rowsContainer.style.webkitTransform = transform;
		} else {
			this.rowsContainer.style.top = `-${renderTop}px`;
		}

269
		this.lastRenderTop = renderTop;
270
		this.lastRenderHeight = renderHeight;
J
Joao Moreno 已提交
271
	}
272

J
Joao Moreno 已提交
273
	// DOM operations
J
Joao Moreno 已提交
274

J
Joao Moreno 已提交
275
	private insertItemInDOM(item: IItem<T>, index: number): void {
J
Joao Moreno 已提交
276 277
		if (!item.row) {
			item.row = this.cache.alloc(item.templateId);
J
Joao Moreno 已提交
278 279
		}

J
Joao Moreno 已提交
280 281
		if (!item.row.domNode.parentElement) {
			this.rowsContainer.appendChild(item.row.domNode);
J
Joao Moreno 已提交
282 283
		}

J
Joao Moreno 已提交
284
		const renderer = this.renderers.get(item.templateId);
J
Johannes Rieken 已提交
285 286
		item.row.domNode.style.top = `${this.elementTop(index)}px`;
		item.row.domNode.style.height = `${item.size}px`;
J
Joao Moreno 已提交
287
		item.row.domNode.setAttribute('data-index', `${index}`);
J
Joao Moreno 已提交
288
		renderer.renderElement(item.element, index, item.row.templateData);
J
Joao Moreno 已提交
289 290
	}

J
Joao Moreno 已提交
291 292 293 294 295
	private updateItemInDOM(item: IItem<T>, index: number): void {
		item.row.domNode.style.top = `${this.elementTop(index)}px`;
		item.row.domNode.setAttribute('data-index', `${index}`);
	}

296
	private removeItemFromDOM(item: IItem<T>): void {
J
Joao Moreno 已提交
297 298
		this.cache.release(item.row);
		item.row = null;
J
Joao Moreno 已提交
299 300
	}

301
	getContentHeight(): number {
J
Joao Moreno 已提交
302 303 304 305
		return this.rangeMap.size;
	}

	getScrollTop(): number {
306 307
		const scrollPosition = this.scrollableElement.getScrollPosition();
		return scrollPosition.scrollTop;
J
Joao Moreno 已提交
308 309 310
	}

	setScrollTop(scrollTop: number): void {
311
		this.scrollableElement.setScrollPosition({ scrollTop });
J
Joao Moreno 已提交
312 313
	}

314 315 316 317 318 319 320 321
	get scrollTop(): number {
		return this.getScrollTop();
	}

	set scrollTop(scrollTop: number) {
		this.setScrollTop(scrollTop);
	}

J
Joao Moreno 已提交
322 323
	// Events

J
Joao Moreno 已提交
324 325 326 327 328 329 330 331 332 333
	@memoize get onMouseClick(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'click'), e => this.toMouseEvent(e)), e => e.index >= 0); }
	@memoize get onMouseDblClick(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'dblclick'), e => this.toMouseEvent(e)), e => e.index >= 0); }
	@memoize get onMouseUp(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mouseup'), e => this.toMouseEvent(e)), e => e.index >= 0); }
	@memoize get onMouseDown(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mousedown'), e => this.toMouseEvent(e)), e => e.index >= 0); }
	@memoize get onMouseOver(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)), e => e.index >= 0); }
	@memoize get onMouseMove(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)), e => e.index >= 0); }
	@memoize get onMouseOut(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)), e => e.index >= 0); }
	@memoize get onContextMenu(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)), e => e.index >= 0); }
	@memoize get onTouchStart(): Event<IListTouchEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'touchstart'), e => this.toTouchEvent(e)), e => e.index >= 0); }
	@memoize get onTap(): Event<IListGestureEvent<T>> { return filterEvent(mapEvent(domEvent(this.rowsContainer, TouchEventType.Tap), e => this.toGestureEvent(e)), e => e.index >= 0); }
J
Joao Moreno 已提交
334

J
Joao Moreno 已提交
335 336 337 338
	private toMouseEvent(browserEvent: MouseEvent): IListMouseEvent<T> {
		const index = this.getItemIndexFromEventTarget(browserEvent.target);
		const element = index < 0 ? undefined : this.items[index].element;
		return { browserEvent, index, element };
339
	}
J
Joao Moreno 已提交
340

J
Joao Moreno 已提交
341 342 343 344 345
	private toTouchEvent(browserEvent: TouchEvent): IListTouchEvent<T> {
		const index = this.getItemIndexFromEventTarget(browserEvent.target);
		const element = index < 0 ? undefined : this.items[index].element;
		return { browserEvent, index, element };
	}
J
Joao Moreno 已提交
346

J
Joao Moreno 已提交
347 348 349 350
	private toGestureEvent(browserEvent: GestureEvent): IListGestureEvent<T> {
		const index = this.getItemIndexFromEventTarget(browserEvent.initialTarget);
		const element = index < 0 ? undefined : this.items[index].element;
		return { browserEvent, index, element };
J
Joao Moreno 已提交
351 352
	}

353 354 355 356
	private onScroll(e: ScrollEvent): void {
		this.render(e.scrollTop, e.height);
	}

357
	private onTouchChange(event: GestureEvent): void {
358 359 360
		event.preventDefault();
		event.stopPropagation();

361
		this.scrollTop -= event.translationY;
362 363 364 365 366
	}

	// Util

	private getItemIndexFromEventTarget(target: EventTarget): number {
J
Joao Moreno 已提交
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
		while (target instanceof HTMLElement && target !== this.rowsContainer) {
			const element = target as HTMLElement;
			const rawIndex = element.getAttribute('data-index');

			if (rawIndex) {
				const index = Number(rawIndex);

				if (!isNaN(index)) {
					return index;
				}
			}

			target = element.parentElement;
		}

		return -1;
	}

385 386 387 388 389 390 391
	private getRenderRange(renderTop: number, renderHeight: number): IRange {
		return {
			start: this.rangeMap.indexAt(renderTop),
			end: this.rangeMap.indexAfter(renderTop + renderHeight - 1)
		};
	}

J
Joao Moreno 已提交
392 393
	// Dispose

J
Joao Moreno 已提交
394 395 396
	dispose() {
		this.items = null;

J
Joao Moreno 已提交
397 398 399
		if (this._domNode && this._domNode.parentElement) {
			this._domNode.parentNode.removeChild(this._domNode);
			this._domNode = null;
J
Joao Moreno 已提交
400 401
		}

402
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
403 404
	}
}