listView.ts 9.8 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.
 *--------------------------------------------------------------------------------------------*/

6
import { IScrollable, ScrollEvent } from 'vs/base/common/scrollable';
J
Joao Moreno 已提交
7
import { Emitter } from 'vs/base/common/event';
J
Joao Moreno 已提交
8
import { toObject, assign } from 'vs/base/common/objects';
J
Joao Moreno 已提交
9
import { IDisposable, dispose } 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 19 20 21 22 23
import { LcsDiff, ISequence } from 'vs/base/common/diff/diff';

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

25
interface IItem<T> {
J
Joao Moreno 已提交
26
	id: string;
27 28
	element: T;
	size: number;
J
Joao Moreno 已提交
29 30
	templateId: string;
	row: IRow;
J
Joao Moreno 已提交
31 32
}

J
Joao Moreno 已提交
33 34 35 36 37 38 39
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 已提交
40 41 42 43 44 45 46 47 48 49 50
const MouseEventTypes = ['click',
	'dblclick',
	'mouseup',
	'mousedown',
	'mouseover',
	'mousemove',
	'mouseout',
	'contextmenu'
];

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

52
	private items: IItem<T>[];
J
Joao Moreno 已提交
53
	private itemId: number;
J
Joao Moreno 已提交
54
	private rangeMap: RangeMap;
J
Joao Moreno 已提交
55
	private cache: RowCache<T>;
J
Joao Moreno 已提交
56
	private renderers: { [templateId: string]: IRenderer<T, any>; };
J
Joao Moreno 已提交
57

J
Joao Moreno 已提交
58
	private renderTop: number;
J
Joao Moreno 已提交
59
	private _renderHeight: number;
J
Joao Moreno 已提交
60

J
Joao Moreno 已提交
61
	private _domNode: HTMLElement;
J
Joao Moreno 已提交
62 63 64 65
	private gesture: Gesture;
	private rowsContainer: HTMLElement;
	private scrollableElement: IScrollableElement;

66
	private _onScroll = new Emitter<ScrollEvent>();
J
Joao Moreno 已提交
67 68 69

	private toDispose: IDisposable[];

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

J
Joao Moreno 已提交
81
		this.renderTop = 0;
J
Joao Moreno 已提交
82
		this._renderHeight = 0;
J
Joao Moreno 已提交
83

J
Joao Moreno 已提交
84 85 86
		this._domNode = document.createElement('div');
		this._domNode.className = 'monaco-list';
		this._domNode.tabIndex = 0;
J
Joao Moreno 已提交
87

J
Joao Moreno 已提交
88 89 90
		this.rowsContainer = document.createElement('div');
		this.rowsContainer.className = 'monaco-list-rows';
		this.gesture = new Gesture(this.rowsContainer);
J
Joao Moreno 已提交
91

92
		this.scrollableElement = new ScrollableElement(this.rowsContainer, this, {
J
Joao Moreno 已提交
93 94 95
			forbidTranslate3dUse: true,
			horizontal: 'hidden',
			vertical: 'auto',
J
Joao Moreno 已提交
96
			useShadows: false,
J
Joao Moreno 已提交
97 98 99
			saveLastScrollTimeOnClassName: 'monaco-list-row'
		});

J
Joao Moreno 已提交
100 101 102 103
		this._domNode.appendChild(this.scrollableElement.getDomNode());
		container.appendChild(this._domNode);

		this.toDispose = [this.rangeMap, this.gesture, this.scrollableElement, this._onScroll];
104 105 106 107

		this.layout();
	}

J
Joao Moreno 已提交
108 109 110 111
	get domNode(): HTMLElement {
		return this._domNode;
	}

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

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

J
Joao Moreno 已提交
125 126 127 128 129 130 131 132 133 134 135 136 137
		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);
			}
		}
138

J
Joao Moreno 已提交
139
		this.rowsContainer.style.height = `${ this.rangeMap.size }px`;
J
Joao Moreno 已提交
140
		this.setScrollTop(this.renderTop);
141
		this._emitScrollEvent(false, false);
J
Joao Moreno 已提交
142 143

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

J
Joao Moreno 已提交
146 147 148 149
	get length(): number {
		return this.items.length;
	}

J
Joao Moreno 已提交
150 151
	get renderHeight(): number {
		return this._renderHeight;
J
Joao Moreno 已提交
152 153
	}

J
Joao Moreno 已提交
154 155 156 157
	element(index: number): T {
		return this.items[index].element;
	}

J
Joao Moreno 已提交
158 159 160 161 162 163 164 165
	elementHeight(index: number): number {
		return this.items[index].size;
	}

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

J
Joao Moreno 已提交
166 167 168 169 170 171 172 173
	indexAt(position: number): number {
		return this.rangeMap.indexAt(position);
	}

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

J
Joao Moreno 已提交
174
	layout(height?: number): void {
J
Joao Moreno 已提交
175
		this.setRenderHeight(height || DOM.getContentHeight(this._domNode));
J
Joao Moreno 已提交
176
		this.setScrollTop(this.renderTop);
J
Joao Moreno 已提交
177
		this.scrollableElement.onElementDimensions();
178
		this._emitScrollEvent(false, false);
J
Joao Moreno 已提交
179 180 181 182
	}

	// Render

J
Joao Moreno 已提交
183 184
	private setRenderHeight(viewHeight: number) {
		this.render(this.renderTop, viewHeight);
J
Joao Moreno 已提交
185
		this._renderHeight = viewHeight;
186 187
	}

J
Joao Moreno 已提交
188 189
	private render(renderTop: number, renderHeight: number): void {
		const renderBottom = renderTop + renderHeight;
J
Joao Moreno 已提交
190
		const thisRenderBottom = this.renderTop + this._renderHeight;
J
Joao Moreno 已提交
191 192 193
		let i: number, stop: number;

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

		// when view scrolls up, start rendering from either this.renderTop or renderBottom
J
Joao Moreno 已提交
199
		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 已提交
200
			this.insertItemInDOM(this.items[i], i);
J
Joao Moreno 已提交
201 202 203
		}

		// when view scrolls down, start unrendering from renderTop
J
Joao Moreno 已提交
204
		for (i = this.rangeMap.indexAt(this.renderTop), stop = Math.min(this.rangeMap.indexAt(renderTop), this.rangeMap.indexAfter(thisRenderBottom)); i < stop; i++) {
205
			this.removeItemFromDOM(this.items[i]);
J
Joao Moreno 已提交
206 207 208
		}

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

J
Joao Moreno 已提交
213
		this.rowsContainer.style.transform = `translate3d(0px, -${ renderTop }px, 0px)`;
J
Joao Moreno 已提交
214
		this.renderTop = renderTop;
J
Joao Moreno 已提交
215
		this._renderHeight = renderBottom - renderTop;
J
Joao Moreno 已提交
216 217
	}

J
Joao Moreno 已提交
218 219
	private getRenderedItemRanges(): IItemRange<T>[] {
		const result: IItemRange<T>[] = [];
J
Joao Moreno 已提交
220
		const renderBottom = this.renderTop + this._renderHeight;
221

J
Joao Moreno 已提交
222 223 224 225
		let start = this.renderTop;
		let index = this.rangeMap.indexAt(start);
		let item = this.items[index];
		let end = -1;
226

J
Joao Moreno 已提交
227
		while (item && start <= renderBottom) {
J
Joao Moreno 已提交
228 229 230 231
			end = start + item.size;
			result.push({ item, index, range: { start, end }});
			start = end;
			item = this.items[++index];
232
		}
J
Joao Moreno 已提交
233

J
Joao Moreno 已提交
234 235
		return result;
	}
236

J
Joao Moreno 已提交
237
	// DOM operations
J
Joao Moreno 已提交
238

J
Joao Moreno 已提交
239
	private insertItemInDOM(item: IItem<T>, index: number): void {
J
Joao Moreno 已提交
240 241
		if (!item.row) {
			item.row = this.cache.alloc(item.templateId);
J
Joao Moreno 已提交
242 243
		}

J
Joao Moreno 已提交
244 245
		if (!item.row.domNode.parentElement) {
			this.rowsContainer.appendChild(item.row.domNode);
J
Joao Moreno 已提交
246 247
		}

J
Joao Moreno 已提交
248
		const renderer = this.renderers[item.templateId];
J
Joao Moreno 已提交
249
		item.row.domNode.style.top = `${ this.elementTop(index) }px`;
J
Joao Moreno 已提交
250
		item.row.domNode.style.height = `${ item.size }px`;
J
Joao Moreno 已提交
251
		item.row.domNode.setAttribute('data-index', `${index}`);
J
Joao Moreno 已提交
252
		renderer.renderElement(item.element, index, item.row.templateData);
J
Joao Moreno 已提交
253 254
	}

255
	private removeItemFromDOM(item: IItem<T>): void {
J
Joao Moreno 已提交
256 257
		this.cache.release(item.row);
		item.row = null;
J
Joao Moreno 已提交
258 259
	}

J
Joao Moreno 已提交
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
	// 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 已提交
283
		scrollTop = Math.min(scrollTop, this.getScrollHeight() - this._renderHeight);
J
Joao Moreno 已提交
284 285
		scrollTop = Math.max(scrollTop, 0);

J
Joao Moreno 已提交
286
		this.render(scrollTop, this._renderHeight);
J
Joao Moreno 已提交
287 288
		this.renderTop = scrollTop;

289 290 291 292 293 294 295 296 297 298 299 300
		this._emitScrollEvent(true, false);
	}

	private _emitScrollEvent(vertical:boolean, horizontal:boolean): void {
		this._onScroll.fire(new ScrollEvent(
			this.getScrollTop(),
			this.getScrollLeft(),
			this.getScrollWidth(),
			this.getScrollHeight(),
			vertical,
			horizontal
		));
J
Joao Moreno 已提交
301 302
	}

303
	addScrollListener(callback: (v:ScrollEvent)=>void): IDisposable {
J
Joao Moreno 已提交
304
		return this._onScroll.event(callback);
J
Joao Moreno 已提交
305 306
	}

J
Joao Moreno 已提交
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 345 346 347 348 349
	// 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 已提交
350 351 352
	dispose() {
		this.items = null;

J
Joao Moreno 已提交
353 354 355
		if (this._domNode && this._domNode.parentElement) {
			this._domNode.parentNode.removeChild(this._domNode);
			this._domNode = null;
J
Joao Moreno 已提交
356 357
		}

J
Joao Moreno 已提交
358
		this.toDispose = dispose(this.toDispose);
J
Joao Moreno 已提交
359 360
	}
}