listView.ts 9.6 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 { toObject, assign, 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';
10
import { domEvent } from 'vs/base/browser/event';
A
Alex Dima 已提交
11
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
J
Joao Moreno 已提交
12
import { ScrollEvent, ScrollbarVisibility } from 'vs/base/common/scrollable';
13
import { RangeMap, IRange, relativeComplement, each } from './rangeMap';
J
Joao Moreno 已提交
14
import { IDelegate, IRenderer, ISpliceable } from './list';
J
Joao Moreno 已提交
15
import { RowCache, IRow } from './rowCache';
16
import { isWindows } from 'vs/base/common/platform';
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
import * as browser from 'vs/base/browser/browser';

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 已提交
40

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

J
Joao Moreno 已提交
49 50
const MouseEventTypes = [
	'click',
J
Joao Moreno 已提交
51 52 53 54 55 56
	'dblclick',
	'mouseup',
	'mousedown',
	'mouseover',
	'mousemove',
	'mouseout',
J
Joao Moreno 已提交
57 58
	'contextmenu',
	'touchstart'
J
Joao Moreno 已提交
59 60
];

J
Joao Moreno 已提交
61 62 63 64 65 66 67 68
export interface IListViewOptions {
	useShadows?: boolean;
}

const DefaultOptions: IListViewOptions = {
	useShadows: true
};

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

71
	private items: IItem<T>[];
J
Joao Moreno 已提交
72
	private itemId: number;
J
Joao Moreno 已提交
73
	private rangeMap: RangeMap;
J
Joao Moreno 已提交
74
	private cache: RowCache<T>;
J
Joao Moreno 已提交
75
	private renderers: { [templateId: string]: IRenderer<T, any>; };
76 77
	private lastRenderTop: number;
	private lastRenderHeight: number;
J
Joao Moreno 已提交
78
	private _domNode: HTMLElement;
J
Joao Moreno 已提交
79 80
	private gesture: Gesture;
	private rowsContainer: HTMLElement;
A
Alex Dima 已提交
81
	private scrollableElement: ScrollableElement;
82
	private disposables: IDisposable[];
J
Joao Moreno 已提交
83

84 85 86
	constructor(
		container: HTMLElement,
		private delegate: IDelegate<T>,
J
Joao Moreno 已提交
87 88
		renderers: IRenderer<T, any>[],
		options: IListViewOptions = DefaultOptions
89
	) {
J
Joao Moreno 已提交
90
		this.items = [];
J
Joao Moreno 已提交
91
		this.itemId = 0;
J
Joao Moreno 已提交
92
		this.rangeMap = new RangeMap();
J
Joao Moreno 已提交
93
		this.renderers = toObject<IRenderer<T, any>>(renderers, r => r.templateId);
J
Joao Moreno 已提交
94
		this.cache = new RowCache(this.renderers);
J
Joao Moreno 已提交
95

96 97
		this.lastRenderTop = 0;
		this.lastRenderHeight = 0;
98

J
Joao Moreno 已提交
99 100
		this._domNode = document.createElement('div');
		this._domNode.className = 'monaco-list';
J
Joao Moreno 已提交
101

J
Joao Moreno 已提交
102 103 104
		this.rowsContainer = document.createElement('div');
		this.rowsContainer.className = 'monaco-list-rows';
		this.gesture = new Gesture(this.rowsContainer);
J
Joao Moreno 已提交
105

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

J
Joao Moreno 已提交
113 114 115
		this._domNode.appendChild(this.scrollableElement.getDomNode());
		container.appendChild(this._domNode);

116 117 118 119
		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);
120 121 122 123

		this.layout();
	}

J
Joao Moreno 已提交
124 125 126 127
	get domNode(): HTMLElement {
		return this._domNode;
	}

J
Joao Moreno 已提交
128
	splice(start: number, deleteCount: number, elements: T[] = []): T[] {
129 130 131
		const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
		each(previousRenderRange, i => this.removeItemFromDOM(this.items[i]));

132
		const inserted = elements.map<IItem<T>>(element => ({
J
Joao Moreno 已提交
133
			id: String(this.itemId++),
134 135 136 137 138 139 140 141
			element,
			size: this.delegate.getHeight(element),
			templateId: this.delegate.getTemplateId(element),
			row: null
		}));

		this.rangeMap.splice(start, deleteCount, ...inserted);

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

144 145
		const renderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
		each(renderRange, i => this.insertItemInDOM(this.items[i], i));
146

J
Joao Moreno 已提交
147
		const scrollHeight = this.getContentHeight();
J
Johannes Rieken 已提交
148
		this.rowsContainer.style.height = `${scrollHeight}px`;
149
		this.scrollableElement.setScrollDimensions({ scrollHeight });
J
Joao Moreno 已提交
150 151

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

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

J
Joao Moreno 已提交
158
	get renderHeight(): number {
159 160
		const scrollDimensions = this.scrollableElement.getScrollDimensions();
		return scrollDimensions.height;
J
Joao Moreno 已提交
161 162
	}

J
Joao Moreno 已提交
163 164 165 166
	element(index: number): T {
		return this.items[index].element;
	}

167 168 169 170 171
	domElement(index: number): HTMLElement {
		const row = this.items[index].row;
		return row && row.domNode;
	}

J
Joao Moreno 已提交
172 173 174 175 176 177 178 179
	elementHeight(index: number): number {
		return this.items[index].size;
	}

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

J
Joao Moreno 已提交
180 181 182 183 184 185 186 187
	indexAt(position: number): number {
		return this.rangeMap.indexAt(position);
	}

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

J
Joao Moreno 已提交
188
	layout(height?: number): void {
189
		this.scrollableElement.setScrollDimensions({
190 191
			height: height || DOM.getContentHeight(this._domNode)
		});
J
Joao Moreno 已提交
192 193 194 195
	}

	// Render

J
Joao Moreno 已提交
196
	private render(renderTop: number, renderHeight: number): void {
197 198
		const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
		const renderRange = this.getRenderRange(renderTop, renderHeight);
J
Joao Moreno 已提交
199

200 201
		const rangesToInsert = relativeComplement(renderRange, previousRenderRange);
		const rangesToRemove = relativeComplement(previousRenderRange, renderRange);
J
Joao Moreno 已提交
202

203 204
		rangesToInsert.forEach(range => each(range, i => this.insertItemInDOM(this.items[i], i)));
		rangesToRemove.forEach(range => each(range, i => this.removeItemFromDOM(this.items[i])));
J
Joao Moreno 已提交
205

206 207 208 209 210 211 212 213
		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`;
		}

214
		this.lastRenderTop = renderTop;
215
		this.lastRenderHeight = renderHeight;
J
Joao Moreno 已提交
216
	}
217

J
Joao Moreno 已提交
218
	// DOM operations
J
Joao Moreno 已提交
219

J
Joao Moreno 已提交
220
	private insertItemInDOM(item: IItem<T>, index: number): void {
J
Joao Moreno 已提交
221 222
		if (!item.row) {
			item.row = this.cache.alloc(item.templateId);
J
Joao Moreno 已提交
223 224
		}

J
Joao Moreno 已提交
225 226
		if (!item.row.domNode.parentElement) {
			this.rowsContainer.appendChild(item.row.domNode);
J
Joao Moreno 已提交
227 228
		}

J
Joao Moreno 已提交
229
		const renderer = this.renderers[item.templateId];
J
Johannes Rieken 已提交
230 231
		item.row.domNode.style.top = `${this.elementTop(index)}px`;
		item.row.domNode.style.height = `${item.size}px`;
J
Joao Moreno 已提交
232
		item.row.domNode.setAttribute('data-index', `${index}`);
J
Joao Moreno 已提交
233
		renderer.renderElement(item.element, index, item.row.templateData);
J
Joao Moreno 已提交
234 235
	}

236
	private removeItemFromDOM(item: IItem<T>): void {
J
Joao Moreno 已提交
237 238
		this.cache.release(item.row);
		item.row = null;
J
Joao Moreno 已提交
239 240
	}

241
	getContentHeight(): number {
J
Joao Moreno 已提交
242 243 244 245
		return this.rangeMap.size;
	}

	getScrollTop(): number {
246 247
		const scrollPosition = this.scrollableElement.getScrollPosition();
		return scrollPosition.scrollTop;
J
Joao Moreno 已提交
248 249 250
	}

	setScrollTop(scrollTop: number): void {
251
		this.scrollableElement.setScrollPosition({ scrollTop });
J
Joao Moreno 已提交
252 253
	}

254 255 256 257 258 259 260 261
	get scrollTop(): number {
		return this.getScrollTop();
	}

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

J
Joao Moreno 已提交
262 263
	// Events

J
Johannes Rieken 已提交
264
	addListener(type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
265 266 267
		const userHandler = handler;
		let domNode = this.domNode;

J
Joao Moreno 已提交
268
		if (MouseEventTypes.indexOf(type) > -1) {
269
			handler = e => this.fireScopedEvent(e, userHandler, this.getItemIndexFromMouseEvent(e));
270 271
		} else if (type === TouchEventType.Tap) {
			domNode = this.rowsContainer;
272
			handler = e => this.fireScopedEvent(e, userHandler, this.getItemIndexFromGestureEvent(e));
273
		}
J
Joao Moreno 已提交
274

275 276
		return DOM.addDisposableListener(domNode, type, handler, useCapture);
	}
J
Joao Moreno 已提交
277

278 279 280 281 282
	private fireScopedEvent(
		event: any,
		handler: (event: any) => void,
		index: number
	) {
283 284
		if (index < 0) {
			return;
J
Joao Moreno 已提交
285 286
		}

287 288
		const element = this.items[index].element;
		handler(assign(event, { element, index }));
J
Joao Moreno 已提交
289 290
	}

291 292 293 294
	private onScroll(e: ScrollEvent): void {
		this.render(e.scrollTop, e.height);
	}

295
	private onTouchChange(event: GestureEvent): void {
296 297 298
		event.preventDefault();
		event.stopPropagation();

299
		this.scrollTop -= event.translationY;
300 301 302 303 304 305 306 307 308 309 310
	}

	// Util

	private getItemIndexFromMouseEvent(event: MouseEvent): number {
		return this.getItemIndexFromEventTarget(event.target);
	}

	private getItemIndexFromGestureEvent(event: GestureEvent): number {
		return this.getItemIndexFromEventTarget(event.initialTarget);
	}
J
Joao Moreno 已提交
311

312
	private getItemIndexFromEventTarget(target: EventTarget): number {
J
Joao Moreno 已提交
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
		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;
	}

331 332 333 334 335 336 337
	private getRenderRange(renderTop: number, renderHeight: number): IRange {
		return {
			start: this.rangeMap.indexAt(renderTop),
			end: this.rangeMap.indexAfter(renderTop + renderHeight - 1)
		};
	}

J
Joao Moreno 已提交
338 339
	// Dispose

J
Joao Moreno 已提交
340 341 342
	dispose() {
		this.items = null;

J
Joao Moreno 已提交
343 344 345
		if (this._domNode && this._domNode.parentElement) {
			this._domNode.parentNode.removeChild(this._domNode);
			this._domNode = null;
J
Joao Moreno 已提交
346 347
		}

348
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
349 350
	}
}