listView.ts 7.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';
J
Joao Moreno 已提交
8 9
import { Gesture } from 'vs/base/browser/touch';
import * as DOM from 'vs/base/browser/dom';
A
Alex Dima 已提交
10
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
11
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
12
import { RangeMap, IRange, relativeComplement, each } from './rangeMap';
J
Joao Moreno 已提交
13
import { IDelegate, IRenderer } from './list';
J
Joao Moreno 已提交
14
import { RowCache, IRow } from './rowCache';
J
Joao Moreno 已提交
15 16 17 18 19 20

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

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

J
Joao Moreno 已提交
30 31
const MouseEventTypes = [
	'click',
J
Joao Moreno 已提交
32 33 34 35 36 37 38 39 40
	'dblclick',
	'mouseup',
	'mousedown',
	'mouseover',
	'mousemove',
	'mouseout',
	'contextmenu'
];

J
Joao Moreno 已提交
41 42 43 44 45 46 47 48
export interface IListViewOptions {
	useShadows?: boolean;
}

const DefaultOptions: IListViewOptions = {
	useShadows: true
};

49
export class ListView<T> implements IDisposable {
J
Joao Moreno 已提交
50

51
	private items: IItem<T>[];
J
Joao Moreno 已提交
52
	private itemId: number;
J
Joao Moreno 已提交
53
	private rangeMap: RangeMap;
J
Joao Moreno 已提交
54
	private cache: RowCache<T>;
J
Joao Moreno 已提交
55
	private renderers: { [templateId: string]: IRenderer<T, any>; };
56 57
	private lastRenderTop: number;
	private lastRenderHeight: number;
J
Joao Moreno 已提交
58
	private _domNode: HTMLElement;
J
Joao Moreno 已提交
59 60
	private gesture: Gesture;
	private rowsContainer: HTMLElement;
A
Alex Dima 已提交
61
	private scrollableElement: ScrollableElement;
J
Joao Moreno 已提交
62 63
	private toDispose: IDisposable[];

64 65 66
	constructor(
		container: HTMLElement,
		private delegate: IDelegate<T>,
J
Joao Moreno 已提交
67 68
		renderers: IRenderer<T, any>[],
		options: IListViewOptions = DefaultOptions
69
	) {
J
Joao Moreno 已提交
70
		this.items = [];
J
Joao Moreno 已提交
71
		this.itemId = 0;
J
Joao Moreno 已提交
72
		this.rangeMap = new RangeMap();
J
Joao Moreno 已提交
73
		this.renderers = toObject<IRenderer<T, any>, IRenderer<T, any>>(renderers, r => r.templateId);
J
Joao Moreno 已提交
74
		this.cache = new RowCache(this.renderers);
J
Joao Moreno 已提交
75

76 77
		this.lastRenderTop = 0;
		this.lastRenderHeight = 0;
78

J
Joao Moreno 已提交
79 80
		this._domNode = document.createElement('div');
		this._domNode.className = 'monaco-list';
J
Joao Moreno 已提交
81

J
Joao Moreno 已提交
82 83 84
		this.rowsContainer = document.createElement('div');
		this.rowsContainer.className = 'monaco-list-rows';
		this.gesture = new Gesture(this.rowsContainer);
J
Joao Moreno 已提交
85

86
		this.scrollableElement = new ScrollableElement(this.rowsContainer, {
87
			canUseTranslate3d: false,
A
Alex Dima 已提交
88 89
			horizontal: ScrollbarVisibility.Hidden,
			vertical: ScrollbarVisibility.Auto,
J
Joao Moreno 已提交
90
			useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows),
J
Joao Moreno 已提交
91 92
			saveLastScrollTimeOnClassName: 'monaco-list-row'
		});
J
Joao Moreno 已提交
93 94

		const listener = this.scrollableElement.onScroll(e => this.render(e.scrollTop, e.height));
J
Joao Moreno 已提交
95

J
Joao Moreno 已提交
96 97 98
		this._domNode.appendChild(this.scrollableElement.getDomNode());
		container.appendChild(this._domNode);

J
Joao Moreno 已提交
99
		this.toDispose = [this.rangeMap, this.gesture, listener, this.scrollableElement];
100 101 102 103

		this.layout();
	}

J
Joao Moreno 已提交
104 105 106 107
	get domNode(): HTMLElement {
		return this._domNode;
	}

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

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

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

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

124 125
		const renderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
		each(renderRange, i => this.insertItemInDOM(this.items[i], i));
126

J
Joao Moreno 已提交
127 128 129
		const scrollHeight = this.getContentHeight();
		this.rowsContainer.style.height = `${ scrollHeight }px`;
		this.scrollableElement.updateState({ scrollHeight });
J
Joao Moreno 已提交
130 131

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

J
Joao Moreno 已提交
134 135 136 137
	get length(): number {
		return this.items.length;
	}

J
Joao Moreno 已提交
138
	get renderHeight(): number {
139
		return this.scrollableElement.getHeight();
J
Joao Moreno 已提交
140 141
	}

J
Joao Moreno 已提交
142 143 144 145
	element(index: number): T {
		return this.items[index].element;
	}

J
Joao Moreno 已提交
146 147 148 149 150 151 152 153
	elementHeight(index: number): number {
		return this.items[index].size;
	}

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

J
Joao Moreno 已提交
154 155 156 157 158 159 160 161
	indexAt(position: number): number {
		return this.rangeMap.indexAt(position);
	}

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

J
Joao Moreno 已提交
162
	layout(height?: number): void {
163 164 165
		this.scrollableElement.updateState({
			height: height || DOM.getContentHeight(this._domNode)
		});
J
Joao Moreno 已提交
166 167 168 169
	}

	// Render

J
Joao Moreno 已提交
170
	private render(renderTop: number, renderHeight: number): void {
171 172
		const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
		const renderRange = this.getRenderRange(renderTop, renderHeight);
J
Joao Moreno 已提交
173

174 175
		const rangesToInsert = relativeComplement(renderRange, previousRenderRange);
		const rangesToRemove = relativeComplement(previousRenderRange, renderRange);
J
Joao Moreno 已提交
176

177 178
		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 已提交
179

J
Joao Moreno 已提交
180
		this.rowsContainer.style.transform = `translate3d(0px, -${ renderTop }px, 0px)`;
181
		this.lastRenderTop = renderTop;
182
		this.lastRenderHeight = renderHeight;
J
Joao Moreno 已提交
183
	}
184

J
Joao Moreno 已提交
185
	// DOM operations
J
Joao Moreno 已提交
186

J
Joao Moreno 已提交
187
	private insertItemInDOM(item: IItem<T>, index: number): void {
J
Joao Moreno 已提交
188 189
		if (!item.row) {
			item.row = this.cache.alloc(item.templateId);
J
Joao Moreno 已提交
190 191
		}

J
Joao Moreno 已提交
192 193
		if (!item.row.domNode.parentElement) {
			this.rowsContainer.appendChild(item.row.domNode);
J
Joao Moreno 已提交
194 195
		}

J
Joao Moreno 已提交
196
		const renderer = this.renderers[item.templateId];
J
Joao Moreno 已提交
197
		item.row.domNode.style.top = `${ this.elementTop(index) }px`;
J
Joao Moreno 已提交
198
		item.row.domNode.style.height = `${ item.size }px`;
J
Joao Moreno 已提交
199
		item.row.domNode.setAttribute('data-index', `${index}`);
J
Joao Moreno 已提交
200
		renderer.renderElement(item.element, index, item.row.templateData);
J
Joao Moreno 已提交
201 202
	}

203
	private removeItemFromDOM(item: IItem<T>): void {
J
Joao Moreno 已提交
204 205
		this.cache.release(item.row);
		item.row = null;
J
Joao Moreno 已提交
206 207
	}

208
	getContentHeight(): number {
J
Joao Moreno 已提交
209 210 211 212
		return this.rangeMap.size;
	}

	getScrollTop(): number {
213
		return this.scrollableElement.getScrollTop();
J
Joao Moreno 已提交
214 215 216
	}

	setScrollTop(scrollTop: number): void {
J
Joao Moreno 已提交
217
		this.scrollableElement.updateState({ scrollTop });
J
Joao Moreno 已提交
218 219
	}

J
Joao Moreno 已提交
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
	// 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;
	}

261 262 263 264 265 266 267
	private getRenderRange(renderTop: number, renderHeight: number): IRange {
		return {
			start: this.rangeMap.indexAt(renderTop),
			end: this.rangeMap.indexAfter(renderTop + renderHeight - 1)
		};
	}

J
Joao Moreno 已提交
268 269
	// Dispose

J
Joao Moreno 已提交
270 271 272
	dispose() {
		this.items = null;

J
Joao Moreno 已提交
273 274 275
		if (this._domNode && this._domNode.parentElement) {
			this._domNode.parentNode.removeChild(this._domNode);
			this._domNode = null;
J
Joao Moreno 已提交
276 277
		}

J
Joao Moreno 已提交
278
		this.toDispose = dispose(this.toDispose);
J
Joao Moreno 已提交
279 280
	}
}