listView.ts 9.9 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>();
67
	private _lastScrollEvent: ScrollEvent;
J
Joao Moreno 已提交
68 69 70

	private toDispose: IDisposable[];

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

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

85 86
		this._lastScrollEvent = new ScrollEvent(this.getScrollTop(), this.getScrollLeft(), this.getScrollWidth(), this.getScrollHeight());

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

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

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

J
Joao Moreno 已提交
103 104 105 106
		this._domNode.appendChild(this.scrollableElement.getDomNode());
		container.appendChild(this._domNode);

		this.toDispose = [this.rangeMap, this.gesture, this.scrollableElement, this._onScroll];
107 108 109 110

		this.layout();
	}

J
Joao Moreno 已提交
111 112 113 114
	get domNode(): HTMLElement {
		return this._domNode;
	}

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

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

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

J
Joao Moreno 已提交
142
		this.rowsContainer.style.height = `${ this.rangeMap.size }px`;
J
Joao Moreno 已提交
143
		this.setScrollTop(this.renderTop);
144
		this._emitScrollEvent();
J
Joao Moreno 已提交
145 146

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

J
Joao Moreno 已提交
149 150 151 152
	get length(): number {
		return this.items.length;
	}

J
Joao Moreno 已提交
153 154
	get renderHeight(): number {
		return this._renderHeight;
J
Joao Moreno 已提交
155 156
	}

J
Joao Moreno 已提交
157 158 159 160
	element(index: number): T {
		return this.items[index].element;
	}

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

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

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

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

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

	// Render

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

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

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

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

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

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

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

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

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

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

J
Joao Moreno 已提交
237 238
		return result;
	}
239

J
Joao Moreno 已提交
240
	// DOM operations
J
Joao Moreno 已提交
241

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

J
Joao Moreno 已提交
247 248
		if (!item.row.domNode.parentElement) {
			this.rowsContainer.appendChild(item.row.domNode);
J
Joao Moreno 已提交
249 250
		}

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

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

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

J
Joao Moreno 已提交
289
		this.render(scrollTop, this._renderHeight);
J
Joao Moreno 已提交
290 291
		this.renderTop = scrollTop;

292
		this._emitScrollEvent();
293 294
	}

295 296 297
	private _emitScrollEvent(): void {
		this._lastScrollEvent = this._lastScrollEvent.create(this.getScrollTop(), this.getScrollLeft(), this.getScrollWidth(), this.getScrollHeight());
		this._onScroll.fire(this._lastScrollEvent);
J
Joao Moreno 已提交
298 299
	}

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

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

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

J
Joao Moreno 已提交
355
		this.toDispose = dispose(this.toDispose);
J
Joao Moreno 已提交
356 357
	}
}