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

'use strict';

8
import 'vs/css!./panelview';
J
Joao Moreno 已提交
9
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
10 11 12 13
import Event, { Emitter, chain } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
14 15
import { $, append, addClass, removeClass, toggleClass } from 'vs/base/browser/dom';
import { firstIndex } from 'vs/base/common/arrays';
J
Joao Moreno 已提交
16
import { Color, RGBA } from 'vs/base/common/color';
17
import { SplitView, IView } from './splitview2';
J
Joao Moreno 已提交
18 19 20 21 22

export interface IPanelOptions {
	ariaHeaderLabel?: string;
	minimumBodySize?: number;
	maximumBodySize?: number;
J
Joao Moreno 已提交
23
	expanded?: boolean;
J
Joao Moreno 已提交
24 25
}

J
Joao Moreno 已提交
26 27
export interface IPanelStyles {
	dropBackground?: Color;
28 29 30
	headerForeground?: Color;
	headerBackground?: Color;
	headerHighContrastBorder?: Color;
J
Joao Moreno 已提交
31 32
}

J
Joao Moreno 已提交
33 34 35 36
export abstract class Panel implements IView {

	private static HEADER_SIZE = 22;

J
Joao Moreno 已提交
37 38
	private _expanded: boolean;
	private _headerVisible: boolean;
J
Joao Moreno 已提交
39 40 41 42
	private _onDidChange = new Emitter<void>();
	private _minimumBodySize: number;
	private _maximumBodySize: number;
	private ariaHeaderLabel: string;
J
Joao Moreno 已提交
43

44
	private header: HTMLElement;
J
Joao Moreno 已提交
45
	protected disposables: IDisposable[] = [];
J
Joao Moreno 已提交
46

47 48 49 50 51 52 53 54 55
	get draggable(): HTMLElement {
		return this.header;
	}

	private _dropBackground: Color | undefined;
	get dropBackground(): Color | undefined {
		return this._dropBackground;
	}

J
Joao Moreno 已提交
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
	get minimumBodySize(): number {
		return this._minimumBodySize;
	}

	set minimumBodySize(size: number) {
		this._minimumBodySize = size;
		this._onDidChange.fire();
	}

	get maximumBodySize(): number {
		return this._maximumBodySize;
	}

	set maximumBodySize(size: number) {
		this._maximumBodySize = size;
		this._onDidChange.fire();
	}

74 75 76 77
	private get headerSize(): number {
		return this.headerVisible ? Panel.HEADER_SIZE : 0;
	}

J
Joao Moreno 已提交
78
	get minimumSize(): number {
79
		const headerSize = this.headerSize;
J
Joao Moreno 已提交
80 81 82 83
		const expanded = !this.headerVisible || this.expanded;
		const minimumBodySize = expanded ? this._minimumBodySize : 0;

		return headerSize + minimumBodySize;
J
Joao Moreno 已提交
84 85 86
	}

	get maximumSize(): number {
87
		const headerSize = this.headerSize;
J
Joao Moreno 已提交
88 89 90 91
		const expanded = !this.headerVisible || this.expanded;
		const maximumBodySize = expanded ? this._maximumBodySize : 0;

		return headerSize + maximumBodySize;
J
Joao Moreno 已提交
92 93 94 95 96
	}

	readonly onDidChange: Event<void> = this._onDidChange.event;

	constructor(options: IPanelOptions = {}) {
J
Joao Moreno 已提交
97
		this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded;
J
Joao Moreno 已提交
98 99 100
		this.ariaHeaderLabel = options.ariaHeaderLabel || '';
		this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 44;
		this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY;
101
		this.header = $('.panel-header');
J
Joao Moreno 已提交
102 103 104 105 106 107 108 109 110 111 112 113
	}

	get expanded(): boolean {
		return this._expanded;
	}

	set expanded(expanded: boolean) {
		if (this._expanded === !!expanded) {
			return;
		}

		this._expanded = !!expanded;
114
		this.updateHeader();
J
Joao Moreno 已提交
115 116 117 118 119 120 121 122 123 124 125 126 127
		this._onDidChange.fire();
	}

	get headerVisible(): boolean {
		return this._headerVisible;
	}

	set headerVisible(visible: boolean) {
		if (this._headerVisible === !!visible) {
			return;
		}

		this._headerVisible = !!visible;
128
		this.updateHeader();
J
Joao Moreno 已提交
129
		this._onDidChange.fire();
J
Joao Moreno 已提交
130 131 132 133 134
	}

	render(container: HTMLElement): void {
		const panel = append(container, $('.panel'));

135 136 137 138 139 140
		append(panel, this.header);
		this.header.setAttribute('tabindex', '0');
		this.header.setAttribute('role', 'toolbar');
		this.header.setAttribute('aria-label', this.ariaHeaderLabel);
		this.renderHeader(this.header);
		this.updateHeader();
J
Joao Moreno 已提交
141

142
		const onHeaderKeyDown = chain(domEvent(this.header, 'keydown'))
143
			.map(e => new StandardKeyboardEvent(e));
J
Joao Moreno 已提交
144 145

		onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space)
J
Joao Moreno 已提交
146
			.event(() => this.expanded = !this.expanded, null, this.disposables);
J
Joao Moreno 已提交
147 148

		onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow)
J
Joao Moreno 已提交
149
			.event(() => this.expanded = false, null, this.disposables);
J
Joao Moreno 已提交
150 151

		onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow)
J
Joao Moreno 已提交
152
			.event(() => this.expanded = true, null, this.disposables);
J
Joao Moreno 已提交
153

154
		domEvent(this.header, 'click')
J
Joao Moreno 已提交
155
			(() => this.expanded = !this.expanded, null, this.disposables);
156

J
Joao Moreno 已提交
157 158 159 160 161 162 163
		// TODO@Joao move this down to panelview
		// onHeaderKeyDown.filter(e => e.keyCode === KeyCode.UpArrow)
		// 	.event(focusPrevious, this, this.disposables);

		// onHeaderKeyDown.filter(e => e.keyCode === KeyCode.DownArrow)
		// 	.event(focusNext, this, this.disposables);

164 165
		const body = append(panel, $('.panel-body'));
		this.renderBody(body);
J
Joao Moreno 已提交
166 167 168
	}

	layout(size: number): void {
J
Joao Moreno 已提交
169 170
		const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0;
		this.layoutBody(size - headerSize);
J
Joao Moreno 已提交
171 172
	}

173 174 175 176 177
	style(styles: IPanelStyles): void {
		this.header.style.color = styles.headerForeground ? styles.headerForeground.toString() : null;
		this.header.style.backgroundColor = styles.headerBackground ? styles.headerBackground.toString() : null;
		this.header.style.borderTop = styles.headerHighContrastBorder ? `1px solid ${styles.headerHighContrastBorder}` : null;
		this._dropBackground = styles.dropBackground;
J
Joao Moreno 已提交
178 179
	}

180
	private updateHeader(): void {
J
Joao Moreno 已提交
181 182
		const expanded = !this.headerVisible || this.expanded;

183 184
		this.header.style.height = `${this.headerSize}px`;
		this.header.style.lineHeight = `${this.headerSize}px`;
J
Joao Moreno 已提交
185 186 187
		toggleClass(this.header, 'hidden', !this.headerVisible);
		toggleClass(this.header, 'expanded', expanded);
		this.header.setAttribute('aria-expanded', String(expanded));
J
Joao Moreno 已提交
188 189
	}

190
	protected abstract renderHeader(container: HTMLElement): void;
J
Joao Moreno 已提交
191 192 193 194 195 196 197 198
	protected abstract renderBody(container: HTMLElement): void;
	protected abstract layoutBody(size: number): void;

	dispose(): void {
		this.disposables = dispose(this.disposables);
	}
}

J
Joao Moreno 已提交
199 200 201 202 203 204
interface IDndContext {
	draggable: PanelDraggable | null;
}

class PanelDraggable implements IDisposable {

J
Joao Moreno 已提交
205 206
	private static DefaultDragOverBackgroundColor = new Color(new RGBA(128, 128, 128, 0.5));

J
Joao Moreno 已提交
207 208 209 210 211 212 213 214
	// see https://github.com/Microsoft/vscode/issues/14470
	private dragOverCounter = 0;
	private disposables: IDisposable[] = [];

	private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>();
	readonly onDidDrop = this._onDidDrop.event;

	constructor(private panel: Panel, private context: IDndContext) {
215 216 217 218 219
		domEvent(panel.draggable, 'dragstart')(this.onDragStart, this, this.disposables);
		domEvent(panel.draggable, 'dragenter')(this.onDragEnter, this, this.disposables);
		domEvent(panel.draggable, 'dragleave')(this.onDragLeave, this, this.disposables);
		domEvent(panel.draggable, 'dragend')(this.onDragEnd, this, this.disposables);
		domEvent(panel.draggable, 'drop')(this.onDrop, this, this.disposables);
J
Joao Moreno 已提交
220 221 222 223 224
	}

	private onDragStart(e: DragEvent): void {
		e.dataTransfer.effectAllowed = 'move';

225
		const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.draggable.textContent));
J
Joao Moreno 已提交
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 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
		e.dataTransfer.setDragImage(dragImage, -10, -10);
		setTimeout(() => document.body.removeChild(dragImage), 0);

		this.context.draggable = this;
	}

	private onDragEnter(e: DragEvent): void {
		if (!this.context.draggable || this.context.draggable === this) {
			return;
		}

		this.dragOverCounter++;
		this.renderHeader();
	}

	private onDragLeave(e: DragEvent): void {
		if (!this.context.draggable || this.context.draggable === this) {
			return;
		}

		this.dragOverCounter--;

		if (this.dragOverCounter === 0) {
			this.renderHeader();
		}
	}

	private onDragEnd(e: DragEvent): void {
		if (!this.context.draggable) {
			return;
		}

		this.dragOverCounter = 0;
		this.renderHeader();
		this.context.draggable = null;
	}

	private onDrop(e: DragEvent): void {
		if (!this.context.draggable) {
			return;
		}

		this.dragOverCounter = 0;
		this.renderHeader();

		if (this.context.draggable !== this) {
			this._onDidDrop.fire({ from: this.context.draggable.panel, to: this.panel });
		}

		this.context.draggable = null;
	}

	private renderHeader(): void {
J
Joao Moreno 已提交
279 280 281
		let backgroundColor: string = null;

		if (this.dragOverCounter > 0) {
282
			backgroundColor = (this.panel.dropBackground || PanelDraggable.DefaultDragOverBackgroundColor).toString();
J
Joao Moreno 已提交
283 284
		}

285
		this.panel.draggable.style.backgroundColor = backgroundColor;
J
Joao Moreno 已提交
286 287 288 289 290 291 292
	}

	dispose(): void {
		this.disposables = dispose(this.disposables);
	}
}

293 294 295 296 297 298 299 300 301
export class IPanelViewOptions {
	dnd?: boolean;
}

interface IPanelItem {
	panel: Panel;
	disposable: IDisposable;
}

J
Joao Moreno 已提交
302 303
export class PanelView implements IDisposable {

J
Joao Moreno 已提交
304
	private dnd: boolean;
305
	private dndContext: IDndContext = { draggable: null };
306 307
	private el: HTMLElement;
	private panelItems: IPanelItem[] = [];
J
Joao Moreno 已提交
308
	private splitview: SplitView;
309
	private animationTimer: number | null = null;
J
Joao Moreno 已提交
310

J
Joao Moreno 已提交
311 312 313
	private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>();
	readonly onDidDrop: Event<{ from: Panel, to: Panel }> = this._onDidDrop.event;

314
	constructor(private container: HTMLElement, options: IPanelViewOptions = {}) {
J
Joao Moreno 已提交
315
		this.dnd = !!options.dnd;
316
		this.el = append(container, $('.monaco-panel-view'));
317
		this.splitview = new SplitView(this.el);
J
Joao Moreno 已提交
318 319
	}

320
	addPanel(panel: Panel, size: number, index = this.splitview.length): void {
J
Joao Moreno 已提交
321 322 323 324 325 326
		const disposables: IDisposable[] = [];
		panel.onDidChange(this.setupAnimation, this, disposables);

		if (this.dnd) {
			const draggable = new PanelDraggable(panel, this.dndContext);
			disposables.push(draggable);
J
Joao Moreno 已提交
327
			draggable.onDidDrop(this._onDidDrop.fire, this._onDidDrop, disposables);
J
Joao Moreno 已提交
328 329 330
		}

		const panelItem = { panel, disposable: combinedDisposable(disposables) };
J
Joao Moreno 已提交
331

332 333
		this.panelItems.splice(index, 0, panelItem);
		this.splitview.addView(panel, size, index);
J
Joao Moreno 已提交
334 335
	}

336 337
	removePanel(panel: Panel): void {
		const index = firstIndex(this.panelItems, item => item.panel === panel);
J
Joao Moreno 已提交
338

339 340 341 342 343 344 345
		if (index === -1) {
			return;
		}

		this.splitview.removeView(index);
		const panelItem = this.panelItems.splice(index, 1)[0];
		panelItem.disposable.dispose();
J
Joao Moreno 已提交
346 347
	}

J
Joao Moreno 已提交
348 349 350 351 352 353 354 355 356 357 358
	movePanel(from: Panel, to: Panel): void {
		const fromIndex = firstIndex(this.panelItems, item => item.panel === from);
		const toIndex = firstIndex(this.panelItems, item => item.panel === to);

		if (fromIndex === -1 || toIndex === -1) {
			return;
		}

		this.splitview.moveView(fromIndex, toIndex);
	}

J
Joao Moreno 已提交
359
	layout(size: number): void {
360 361 362 363 364 365 366 367 368
		this.splitview.layout(size);
	}

	private setupAnimation(): void {
		if (typeof this.animationTimer === 'number') {
			window.clearTimeout(this.animationTimer);
		}

		addClass(this.el, 'animated');
J
Joao Moreno 已提交
369

370 371 372 373
		this.animationTimer = window.setTimeout(() => {
			this.animationTimer = null;
			removeClass(this.el, 'animated');
		}, 200);
J
Joao Moreno 已提交
374 375 376
	}

	dispose(): void {
J
Joao Moreno 已提交
377 378
		this.panelItems.forEach(i => i.disposable.dispose());
		this.splitview.dispose();
J
Joao Moreno 已提交
379 380
	}
}