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

import { IScrollable } from 'vs/base/common/scrollable';
J
Joao Moreno 已提交
7
import { Emitter } from 'vs/base/common/event';
J
Joao Moreno 已提交
8 9
import { toObject, assign } from 'vs/base/common/objects';
import { IDisposable, disposeAll } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
10 11 12
import { Gesture } from 'vs/base/browser/touch';
import * as DOM from 'vs/base/browser/dom';
import { IScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
J
Joao Moreno 已提交
13
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElementImpl';
J
Joao Moreno 已提交
14
import { RangeMap, IRange } from './rangeMap';
J
Joao Moreno 已提交
15
import { IDelegate, IRenderer } from './list';
J
Joao Moreno 已提交
16
import { RowCache, IRow } from './rowCache';
J
Joao Moreno 已提交
17 18
import { LcsDiff, ISequence } from 'vs/base/common/diff/diff';

J
Joao Moreno 已提交
19 20 21 22 23
interface IScrollEvent {
	vertical: boolean;
	horizontal: boolean;
}

J
Joao Moreno 已提交
24 25 26 27 28
interface IItemRange<T> {
	item: IItem<T>;
	index: number;
	range: IRange;
}
J
Joao Moreno 已提交
29

30
interface IItem<T> {
J
Joao Moreno 已提交
31
	id: string;
32 33
	element: T;
	size: number;
J
Joao Moreno 已提交
34 35
	templateId: string;
	row: IRow;
J
Joao Moreno 已提交
36 37
}

J
Joao Moreno 已提交
38 39 40 41 42 43 44
function toSequence<T>(itemRanges: IItemRange<T>[]): ISequence {
	return {
		getLength: () => itemRanges.length,
		getElementHash: i => `${ itemRanges[i].item.id }:${ itemRanges[i].range.start }:${ itemRanges[i].range.end }`
	};
}

J
Joao Moreno 已提交
45 46 47 48 49 50 51 52 53 54 55
const MouseEventTypes = ['click',
	'dblclick',
	'mouseup',
	'mousedown',
	'mouseover',
	'mousemove',
	'mouseout',
	'contextmenu'
];

export class ListView<T> implements IScrollable, IDisposable {
J
Joao Moreno 已提交
56

57
	private items: IItem<T>[];
J
Joao Moreno 已提交
58
	private itemId: number;
J
Joao Moreno 已提交
59
	private rangeMap: RangeMap;
J
Joao Moreno 已提交
60
	private cache: RowCache<T>;
J
Joao Moreno 已提交
61
	private renderers: { [templateId: string]: IRenderer<T, any>; };
J
Joao Moreno 已提交
62

J
Joao Moreno 已提交
63
	private renderTop: number;
J
Joao Moreno 已提交
64
	private _renderHeight: number;
J
Joao Moreno 已提交
65

J
Joao Moreno 已提交
66
	private _domNode: HTMLElement;
J
Joao Moreno 已提交
67 68 69 70
	private gesture: Gesture;
	private rowsContainer: HTMLElement;
	private scrollableElement: IScrollableElement;

J
Joao Moreno 已提交
71
	private _onScroll = new Emitter<IScrollEvent>();
J
Joao Moreno 已提交
72 73 74

	private toDispose: IDisposable[];

75 76 77
	constructor(
		container: HTMLElement,
		private delegate: IDelegate<T>,
J
Joao Moreno 已提交
78
		renderers: IRenderer<T, any>[]
79
	) {
J
Joao Moreno 已提交
80
		this.items = [];
J
Joao Moreno 已提交
81
		this.itemId = 0;
J
Joao Moreno 已提交
82
		this.rangeMap = new RangeMap();
J
Joao Moreno 已提交
83 84
		this.renderers = toObject(renderers, r => r.templateId);
		this.cache = new RowCache(this.renderers);
J
Joao Moreno 已提交
85

J
Joao Moreno 已提交
86
		this.renderTop = 0;
J
Joao Moreno 已提交
87
		this._renderHeight = 0;
J
Joao Moreno 已提交
88

J
Joao Moreno 已提交
89 90 91
		this._domNode = document.createElement('div');
		this._domNode.className = 'monaco-list';
		this._domNode.tabIndex = 0;
J
Joao Moreno 已提交
92

J
Joao Moreno 已提交
93 94 95
		this.rowsContainer = document.createElement('div');
		this.rowsContainer.className = 'monaco-list-rows';
		this.gesture = new Gesture(this.rowsContainer);
J
Joao Moreno 已提交
96

J
Joao Moreno 已提交
97
		this.scrollableElement = new ScrollableElement(this.rowsContainer, {
J
Joao Moreno 已提交
98 99 100 101
			forbidTranslate3dUse: true,
			scrollable: this,
			horizontal: 'hidden',
			vertical: 'auto',
J
Joao Moreno 已提交
102
			useShadows: false,
J
Joao Moreno 已提交
103 104 105
			saveLastScrollTimeOnClassName: 'monaco-list-row'
		});

J
Joao Moreno 已提交
106 107 108 109
		this._domNode.appendChild(this.scrollableElement.getDomNode());
		container.appendChild(this._domNode);

		this.toDispose = [this.rangeMap, this.gesture, this.scrollableElement, this._onScroll];
110 111 112 113

		this.layout();
	}

J
Joao Moreno 已提交
114 115 116 117
	get domNode(): HTMLElement {
		return this._domNode;
	}

J
Joao Moreno 已提交
118
	splice(start: number, deleteCount: number, ...elements: T[]): T[] {
J
Joao Moreno 已提交
119
		const before = this.getRenderedItemRanges();
120
		const inserted = elements.map<IItem<T>>(element => ({
J
Joao Moreno 已提交
121
			id: String(this.itemId++),
122 123 124 125 126 127 128
			element,
			size: this.delegate.getHeight(element),
			templateId: this.delegate.getTemplateId(element),
			row: null
		}));

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

J
Joao Moreno 已提交
131 132 133 134 135 136 137 138 139 140 141 142 143
		const after = this.getRenderedItemRanges();
		const lcs = new LcsDiff(toSequence(before), toSequence(after), null);
		const diffs = lcs.ComputeDiff();

		for (const diff of diffs) {
			for (let i = 0; i < diff.originalLength; i++) {
				this.removeItemFromDOM(before[diff.originalStart + i].item);
			}

			for (let i = 0; i < diff.modifiedLength; i++) {
				this.insertItemInDOM(after[diff.modifiedStart + i].item, after[0].index + diff.modifiedStart + i);
			}
		}
144

J
Joao Moreno 已提交
145
		this.rowsContainer.style.height = `${ this.rangeMap.size }px`;
J
Joao Moreno 已提交
146
		this.setScrollTop(this.renderTop);
147
		this.scrollableElement.onElementInternalDimensions();
J
Joao Moreno 已提交
148 149

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

J
Joao Moreno 已提交
152 153 154 155
	get length(): number {
		return this.items.length;
	}

J
Joao Moreno 已提交
156 157
	get renderHeight(): number {
		return this._renderHeight;
J
Joao Moreno 已提交
158 159
	}

J
Joao Moreno 已提交
160 161 162 163
	element(index: number): T {
		return this.items[index].element;
	}

J
Joao Moreno 已提交
164 165 166 167 168 169 170 171
	elementHeight(index: number): number {
		return this.items[index].size;
	}

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

J
Joao Moreno 已提交
172 173 174 175 176 177 178 179
	indexAt(position: number): number {
		return this.rangeMap.indexAt(position);
	}

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

J
Joao Moreno 已提交
180
	layout(height?: number): void {
J
Joao Moreno 已提交
181
		this.setRenderHeight(height || DOM.getContentHeight(this._domNode));
J
Joao Moreno 已提交
182
		this.setScrollTop(this.renderTop);
J
Joao Moreno 已提交
183 184 185 186 187 188
		this.scrollableElement.onElementDimensions();
		this.scrollableElement.onElementInternalDimensions();
	}

	// Render

J
Joao Moreno 已提交
189 190
	private setRenderHeight(viewHeight: number) {
		this.render(this.renderTop, viewHeight);
J
Joao Moreno 已提交
191
		this._renderHeight = viewHeight;
192 193
	}

J
Joao Moreno 已提交
194 195
	private render(renderTop: number, renderHeight: number): void {
		const renderBottom = renderTop + renderHeight;
J
Joao Moreno 已提交
196
		const thisRenderBottom = this.renderTop + this._renderHeight;
J
Joao Moreno 已提交
197 198 199
		let i: number, stop: number;

		// when view scrolls down, start rendering from the renderBottom
J
Joao Moreno 已提交
200
		for (i = this.rangeMap.indexAfter(renderBottom) - 1, stop = this.rangeMap.indexAt(Math.max(thisRenderBottom, renderTop)); i >= stop; i--) {
J
Joao Moreno 已提交
201
			this.insertItemInDOM(this.items[i], i);
J
Joao Moreno 已提交
202 203 204
		}

		// when view scrolls up, start rendering from either this.renderTop or renderBottom
J
Joao Moreno 已提交
205
		for (i = Math.min(this.rangeMap.indexAt(this.renderTop), this.rangeMap.indexAfter(renderBottom)) - 1, stop = this.rangeMap.indexAt(renderTop); i >= stop; i--) {
J
Joao Moreno 已提交
206
			this.insertItemInDOM(this.items[i], i);
J
Joao Moreno 已提交
207 208 209
		}

		// when view scrolls down, start unrendering from renderTop
J
Joao Moreno 已提交
210
		for (i = this.rangeMap.indexAt(this.renderTop), stop = Math.min(this.rangeMap.indexAt(renderTop), this.rangeMap.indexAfter(thisRenderBottom)); i < stop; i++) {
211
			this.removeItemFromDOM(this.items[i]);
J
Joao Moreno 已提交
212 213 214
		}

		// when view scrolls up, start unrendering from either renderBottom this.renderTop
J
Joao Moreno 已提交
215
		for (i = Math.max(this.rangeMap.indexAfter(renderBottom), this.rangeMap.indexAt(this.renderTop)), stop = this.rangeMap.indexAfter(thisRenderBottom); i < stop; i++) {
216
			this.removeItemFromDOM(this.items[i]);
J
Joao Moreno 已提交
217 218
		}

J
Joao Moreno 已提交
219
		this.rowsContainer.style.transform = `translate3d(0px, -${ renderTop }px, 0px)`;
J
Joao Moreno 已提交
220
		this.renderTop = renderTop;
J
Joao Moreno 已提交
221
		this._renderHeight = renderBottom - renderTop;
J
Joao Moreno 已提交
222 223
	}

J
Joao Moreno 已提交
224 225
	private getRenderedItemRanges(): IItemRange<T>[] {
		const result: IItemRange<T>[] = [];
J
Joao Moreno 已提交
226
		const renderBottom = this.renderTop + this._renderHeight;
227

J
Joao Moreno 已提交
228 229 230 231
		let start = this.renderTop;
		let index = this.rangeMap.indexAt(start);
		let item = this.items[index];
		let end = -1;
232

J
Joao Moreno 已提交
233
		while (item && start <= renderBottom) {
J
Joao Moreno 已提交
234 235 236 237
			end = start + item.size;
			result.push({ item, index, range: { start, end }});
			start = end;
			item = this.items[++index];
238
		}
J
Joao Moreno 已提交
239

J
Joao Moreno 已提交
240 241
		return result;
	}
242

J
Joao Moreno 已提交
243
	// DOM operations
J
Joao Moreno 已提交
244

J
Joao Moreno 已提交
245
	private insertItemInDOM(item: IItem<T>, index: number): void {
J
Joao Moreno 已提交
246 247
		if (!item.row) {
			item.row = this.cache.alloc(item.templateId);
J
Joao Moreno 已提交
248 249
		}

J
Joao Moreno 已提交
250 251
		if (!item.row.domNode.parentElement) {
			this.rowsContainer.appendChild(item.row.domNode);
J
Joao Moreno 已提交
252 253
		}

J
Joao Moreno 已提交
254
		const renderer = this.renderers[item.templateId];
J
Joao Moreno 已提交
255
		item.row.domNode.style.top = `${ this.elementTop(index) }px`;
J
Joao Moreno 已提交
256
		item.row.domNode.style.height = `${ item.size }px`;
J
Joao Moreno 已提交
257
		item.row.domNode.setAttribute('data-index', `${index}`);
J
Joao Moreno 已提交
258
		renderer.renderElement(item.element, index, item.row.templateData);
J
Joao Moreno 已提交
259 260
	}

261
	private removeItemFromDOM(item: IItem<T>): void {
J
Joao Moreno 已提交
262 263
		this.cache.release(item.row);
		item.row = null;
J
Joao Moreno 已提交
264 265
	}

J
Joao Moreno 已提交
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
	// IScrollable

	getScrollHeight(): number {
		return this.rangeMap.size;
	}

	getScrollWidth(): number {
		return 0;
	}

	getScrollLeft(): number {
		return 0;
	}

	setScrollLeft(scrollLeft: number): void {
		// noop
	}

	getScrollTop(): number {
		return this.renderTop;
	}

	setScrollTop(scrollTop: number): void {
J
Joao Moreno 已提交
289
		scrollTop = Math.min(scrollTop, this.getScrollHeight() - this._renderHeight);
J
Joao Moreno 已提交
290 291
		scrollTop = Math.max(scrollTop, 0);

J
Joao Moreno 已提交
292
		this.render(scrollTop, this._renderHeight);
J
Joao Moreno 已提交
293 294
		this.renderTop = scrollTop;

J
Joao Moreno 已提交
295
		this._onScroll.fire({ vertical: true, horizontal: false });
J
Joao Moreno 已提交
296 297 298
	}

	addScrollListener(callback: ()=>void): IDisposable {
J
Joao Moreno 已提交
299
		return this._onScroll.event(callback);
J
Joao Moreno 已提交
300 301
	}

J
Joao Moreno 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
	// Events

	addListener(type: string, handler: (event:any)=>void, useCapture?: boolean): IDisposable {
		if (MouseEventTypes.indexOf(type) > -1) {
			const userHandler = handler;
			handler = (event: MouseEvent) => {
				const index = this.getItemIndex(event);

				if (index < 0) {
					return;
				}

				const element = this.items[index].element;
				userHandler(assign(event, { element, index }));
			};
		}

		return DOM.addDisposableListener(this.domNode, type, handler, useCapture);
	}

	private getItemIndex(event: MouseEvent): number {
		let target = event.target;

		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;
	}

	// Dispose

J
Joao Moreno 已提交
345 346 347
	dispose() {
		this.items = null;

J
Joao Moreno 已提交
348 349 350
		if (this._domNode && this._domNode.parentElement) {
			this._domNode.parentNode.removeChild(this._domNode);
			this._domNode = null;
J
Joao Moreno 已提交
351 352
		}

J
Joao Moreno 已提交
353
		this.toDispose = disposeAll(this.toDispose);
J
Joao Moreno 已提交
354 355
	}
}