inputBox.ts 11.1 KB
Newer Older
E
Erich Gamma 已提交
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';

import 'vs/css!./inputBox';
A
Alex Dima 已提交
8

9
import nls = require('vs/nls');
A
Alex Dima 已提交
10 11
import * as Bal from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
12 13
import { IHTMLContentElement } from 'vs/base/common/htmlContent';
import { renderHtml } from 'vs/base/browser/htmlContentRenderer';
14
import aria = require('vs/base/browser/ui/aria/aria');
J
Johannes Rieken 已提交
15 16 17 18 19
import { IAction } from 'vs/base/common/actions';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IContextViewProvider, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import Event, { Emitter } from 'vs/base/common/event';
import { Widget } from 'vs/base/browser/ui/widget';
B
Benjamin Pasero 已提交
20
import { Color } from 'vs/base/common/color';
A
Alex Dima 已提交
21

J
Joao Moreno 已提交
22
const $ = dom.$;
E
Erich Gamma 已提交
23

B
Benjamin Pasero 已提交
24
export interface IInputOptions extends IInputBoxStyles {
25 26 27 28
	placeholder?: string;
	ariaLabel?: string;
	type?: string;
	validationOptions?: IInputValidationOptions;
E
Erich Gamma 已提交
29
	flexibleHeight?: boolean;
30
	actions?: IAction[];
E
Erich Gamma 已提交
31 32
}

33 34 35 36 37
export interface IInputBoxStyles {
	inputBackground?: Color;
	inputForeground?: Color;
}

E
Erich Gamma 已提交
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
export interface IInputValidator {
	(value: string): IMessage;
}

export interface IMessage {
	content: string;
	formatContent?: boolean; // defaults to false
	type?: MessageType;
}

export interface IInputValidationOptions {
	validation: IInputValidator;
	showMessage?: boolean;
}

export enum MessageType {
	INFO = 1,
	WARNING = 2,
	ERROR = 3
}

export interface IRange {
	start: number;
	end: number;
}

A
Alex Dima 已提交
64 65
export class InputBox extends Widget {
	private contextViewProvider: IContextViewProvider;
E
Erich Gamma 已提交
66 67 68
	private element: HTMLElement;
	private input: HTMLInputElement;
	private mirror: HTMLElement;
A
Alex Dima 已提交
69
	private actionbar: ActionBar;
E
Erich Gamma 已提交
70 71 72 73 74 75 76 77 78
	private options: IInputOptions;
	private message: IMessage;
	private placeholder: string;
	private ariaLabel: string;
	private validation: IInputValidator;
	private showValidationMessage: boolean;
	private state = 'idle';
	private cachedHeight: number;

79 80 81
	private inputBackground: Color;
	private inputForeground: Color;

A
Alex Dima 已提交
82 83 84 85 86 87
	private _onDidChange = this._register(new Emitter<string>());
	public onDidChange: Event<string> = this._onDidChange.event;

	private _onDidHeightChange = this._register(new Emitter<number>());
	public onDidHeightChange: Event<number> = this._onDidHeightChange.event;

88
	constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, options?: IInputOptions) {
E
Erich Gamma 已提交
89 90 91 92 93 94 95 96
		super();

		this.contextViewProvider = contextViewProvider;
		this.options = options || Object.create(null);
		this.message = null;
		this.cachedHeight = null;
		this.placeholder = this.options.placeholder || '';
		this.ariaLabel = this.options.ariaLabel || '';
97 98
		this.inputBackground = this.options.inputBackground;
		this.inputForeground = this.options.inputForeground;
E
Erich Gamma 已提交
99 100 101 102 103 104 105 106

		if (this.options.validationOptions) {
			this.validation = this.options.validationOptions.validation;
			this.showValidationMessage = this.options.validationOptions.showMessage || false;
		}

		this.element = dom.append(container, $('.monaco-inputbox.idle'));

A
Alex Dima 已提交
107
		let tagName = this.options.flexibleHeight ? 'textarea' : 'input';
E
Erich Gamma 已提交
108

A
Alex Dima 已提交
109
		let wrapper = dom.append(this.element, $('.wrapper'));
110
		this.input = <HTMLInputElement>dom.append(wrapper, $(tagName + '.input'));
E
Erich Gamma 已提交
111 112 113
		this.input.setAttribute('autocorrect', 'off');
		this.input.setAttribute('autocapitalize', 'off');
		this.input.setAttribute('spellcheck', 'false');
114

115 116
		this.onfocus(this.input, () => dom.addClass(this.element, 'synthetic-focus'));
		this.onblur(this.input, () => dom.removeClass(this.element, 'synthetic-focus'));
E
Erich Gamma 已提交
117 118 119 120 121 122 123 124 125 126 127 128 129 130

		if (this.options.flexibleHeight) {
			this.mirror = dom.append(wrapper, $('div.mirror'));
		} else {
			this.input.type = this.options.type || 'text';
			this.input.setAttribute('wrap', 'off');
		}

		if (this.ariaLabel) {
			this.input.setAttribute('aria-label', this.ariaLabel);
		}

		if (this.placeholder) {
			this.input.setAttribute('placeholder', this.placeholder);
J
Joao Moreno 已提交
131
			this.input.title = this.placeholder;
E
Erich Gamma 已提交
132 133
		}

A
Alex Dima 已提交
134 135 136
		this.oninput(this.input, () => this.onValueChange());
		this.onblur(this.input, () => this.onBlur());
		this.onfocus(this.input, () => this.onFocus());
E
Erich Gamma 已提交
137 138

		// Add placeholder shim for IE because IE decides to hide the placeholder on focus (we dont want that!)
A
Alex Dima 已提交
139
		if (this.placeholder && Bal.isIE) {
A
Alex Dima 已提交
140
			this.onclick(this.input, (e) => {
E
Erich Gamma 已提交
141 142
				dom.EventHelper.stop(e, true);
				this.input.focus();
A
Alex Dima 已提交
143
			});
E
Erich Gamma 已提交
144 145
		}

J
Joao Moreno 已提交
146
		setTimeout(() => this.updateMirror(), 0);
E
Erich Gamma 已提交
147 148 149

		// Support actions
		if (this.options.actions) {
A
Alex Dima 已提交
150
			this.actionbar = this._register(new ActionBar(this.element));
E
Erich Gamma 已提交
151 152
			this.actionbar.push(this.options.actions, { icon: true, label: false });
		}
B
Benjamin Pasero 已提交
153 154

		this._applyStyles();
E
Erich Gamma 已提交
155 156 157 158 159 160 161 162 163 164
	}

	private onBlur(): void {
		this._hideMessage();
	}

	private onFocus(): void {
		this._showMessage();
	}

165
	public setPlaceHolder(placeHolder: string): void {
E
Erich Gamma 已提交
166 167 168 169 170
		if (this.input) {
			this.input.setAttribute('placeholder', placeHolder);
		}
	}

171 172 173 174 175 176 177 178 179 180 181 182
	public setAriaLabel(label: string): void {
		this.ariaLabel = label;

		if (this.input) {
			if (label) {
				this.input.setAttribute('aria-label', this.ariaLabel);
			} else {
				this.input.removeAttribute('aria-label');
			}
		}
	}

A
Alex Dima 已提交
183
	public setContextViewProvider(contextViewProvider: IContextViewProvider): void {
E
Erich Gamma 已提交
184 185 186 187 188 189 190
		this.contextViewProvider = contextViewProvider;
	}

	public get inputElement(): HTMLInputElement {
		return this.input;
	}

191
	public get value(): string {
E
Erich Gamma 已提交
192 193 194
		return this.input.value;
	}

195
	public set value(newValue: string) {
E
Erich Gamma 已提交
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
		if (this.input.value !== newValue) {
			this.input.value = newValue;
			this.onValueChange();
		}
	}

	public get height(): number {
		return this.cachedHeight === null ? dom.getTotalHeight(this.element) : this.cachedHeight;
	}

	public focus(): void {
		this.input.focus();
	}

	public blur(): void {
		this.input.blur();
	}

	public hasFocus(): boolean {
215
		return document.activeElement === this.input;
E
Erich Gamma 已提交
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
	}

	public select(range: IRange = null): void {
		this.input.select();

		if (range) {
			this.input.setSelectionRange(range.start, range.end);
		}
	}

	public enable(): void {
		this.input.removeAttribute('disabled');
	}

	public disable(): void {
		this.input.disabled = true;
		this._hideMessage();
	}

235
	public setEnabled(enabled: boolean): void {
236 237 238 239 240 241 242
		if (enabled) {
			this.enable();
		} else {
			this.disable();
		}
	}

243
	public get width(): number {
E
Erich Gamma 已提交
244 245 246
		return dom.getTotalWidth(this.input);
	}

247
	public set width(width: number) {
E
Erich Gamma 已提交
248 249 250
		this.input.style.width = width + 'px';
	}

251
	public showMessage(message: IMessage, force?: boolean): void {
E
Erich Gamma 已提交
252 253 254 255 256 257 258 259
		this.message = message;

		dom.removeClass(this.element, 'idle');
		dom.removeClass(this.element, 'info');
		dom.removeClass(this.element, 'warning');
		dom.removeClass(this.element, 'error');
		dom.addClass(this.element, this.classForType(message.type));

260 261 262 263 264 265 266 267 268 269 270 271
		// ARIA Support
		let alertText: string;
		if (message.type === MessageType.ERROR) {
			alertText = nls.localize('alertErrorMessage', "Error: {0}", message.content);
		} else if (message.type === MessageType.WARNING) {
			alertText = nls.localize('alertWarningMessage', "Warning: {0}", message.content);
		} else {
			alertText = nls.localize('alertInfoMessage', "Info: {0}", message.content);
		}

		aria.alert(alertText);

E
Erich Gamma 已提交
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
		if (this.hasFocus() || force) {
			this._showMessage();
		}
	}

	public hideMessage(): void {
		this.message = null;

		dom.removeClass(this.element, 'info');
		dom.removeClass(this.element, 'warning');
		dom.removeClass(this.element, 'error');
		dom.addClass(this.element, 'idle');

		this._hideMessage();
	}

	public isInputValid(): boolean {
		return !!this.validation && !this.validation(this.value);
	}

	public validate(): boolean {
A
Alex Dima 已提交
293
		let result: IMessage = null;
E
Erich Gamma 已提交
294 295 296 297 298

		if (this.validation) {
			result = this.validation(this.value);

			if (!result) {
299
				this.inputElement.removeAttribute('aria-invalid');
E
Erich Gamma 已提交
300 301
				this.hideMessage();
			} else {
302
				this.inputElement.setAttribute('aria-invalid', 'true');
E
Erich Gamma 已提交
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
				this.showMessage(result);
			}
		}

		return !result;
	}

	private classForType(type: MessageType): string {
		switch (type) {
			case MessageType.INFO: return 'info';
			case MessageType.WARNING: return 'warning';
			default: return 'error';
		}
	}

	private _showMessage(): void {
		if (!this.contextViewProvider || !this.message) {
			return;
		}

A
Alex Dima 已提交
323 324
		let div: HTMLElement;
		let layout = () => div.style.width = dom.getTotalWidth(this.element) + 'px';
E
Erich Gamma 已提交
325 326 327 328 329

		this.state = 'open';

		this.contextViewProvider.showContextView({
			getAnchor: () => this.element,
A
Alex Dima 已提交
330
			anchorAlignment: AnchorAlignment.RIGHT,
E
Erich Gamma 已提交
331 332 333 334
			render: (container: HTMLElement) => {
				div = dom.append(container, $('.monaco-inputbox-container'));
				layout();

A
Alex Dima 已提交
335
				let renderOptions: IHTMLContentElement = {
E
Erich Gamma 已提交
336 337 338 339 340 341 342 343 344 345
					tagName: 'span',
					className: 'monaco-inputbox-message',
				};

				if (this.message.formatContent) {
					renderOptions.formattedText = this.message.content;
				} else {
					renderOptions.text = this.message.content;
				}

346
				let spanElement: HTMLElement = <any>renderHtml(renderOptions);
E
Erich Gamma 已提交
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
				dom.addClass(spanElement, this.classForType(this.message.type));
				dom.append(div, spanElement);
				return null;
			},
			layout: layout
		});
	}

	private _hideMessage(): void {
		if (!this.contextViewProvider || this.state !== 'open') {
			return;
		}

		this.state = 'idle';

		this.contextViewProvider.hideContextView();
	}

	private onValueChange(): void {
A
Alex Dima 已提交
366
		this._onDidChange.fire(this.value);
E
Erich Gamma 已提交
367 368

		this.validate();
J
Joao Moreno 已提交
369
		this.updateMirror();
J
Joao Moreno 已提交
370 371 372 373

		if (this.state === 'open') {
			this.contextViewProvider.layout();
		}
E
Erich Gamma 已提交
374 375
	}

J
Joao Moreno 已提交
376 377 378 379 380 381 382 383 384 385 386 387
	private updateMirror(): void {
		if (!this.mirror) {
			return;
		}

		const value = this.value || this.placeholder;
		let lastCharCode = value.charCodeAt(value.length - 1);
		let suffix = lastCharCode === 10 ? ' ' : '';
		this.mirror.textContent = value + suffix;
		this.layout();
	}

B
Benjamin Pasero 已提交
388
	public style(styles: IInputBoxStyles) {
389 390
		this.inputBackground = styles.inputBackground;
		this.inputForeground = styles.inputForeground;
B
Benjamin Pasero 已提交
391 392 393 394 395 396

		this._applyStyles();
	}

	protected _applyStyles() {
		if (this.element) {
397 398
			const background = this.inputBackground ? this.inputBackground.toString() : null;
			const foreground = this.inputForeground ? this.inputForeground.toString() : null;
B
Benjamin Pasero 已提交
399 400 401 402 403 404 405 406

			this.element.style.backgroundColor = background;
			this.element.style.color = foreground;
			this.input.style.backgroundColor = background;
			this.input.style.color = foreground;
		}
	}

407
	public layout(): void {
E
Erich Gamma 已提交
408 409 410 411
		if (!this.mirror) {
			return;
		}

412
		const previousHeight = this.cachedHeight;
E
Erich Gamma 已提交
413 414
		this.cachedHeight = dom.getTotalHeight(this.mirror);

415 416
		if (previousHeight !== this.cachedHeight) {
			this.input.style.height = this.cachedHeight + 'px';
A
Alex Dima 已提交
417
			this._onDidHeightChange.fire(this.cachedHeight);
418
		}
E
Erich Gamma 已提交
419 420 421 422 423 424 425 426 427 428 429 430 431 432
	}

	public dispose(): void {
		this._hideMessage();

		this.element = null;
		this.input = null;
		this.contextViewProvider = null;
		this.message = null;
		this.placeholder = null;
		this.ariaLabel = null;
		this.validation = null;
		this.showValidationMessage = null;
		this.state = null;
A
Alex Dima 已提交
433
		this.actionbar = null;
E
Erich Gamma 已提交
434 435 436

		super.dispose();
	}
437
}