textAreaHandler.ts 13.3 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import 'vs/css!./textAreaHandler';
A
Alex Dima 已提交
8
import * as browser from 'vs/base/browser/browser';
9 10
import { TextAreaInput, ITextAreaInputHost, IPasteData, ICompositionData } from 'vs/editor/browser/controller/textAreaInput';
import { ISimpleModel, ITypeData, TextAreaState, IENarratorStrategy, NVDAPagedStrategy } from 'vs/editor/browser/controller/textAreaState';
J
Johannes Rieken 已提交
11 12 13 14
import { Range } from 'vs/editor/common/core/range';
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ViewContext } from 'vs/editor/common/view/viewContext';
15
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
16
import * as viewEvents from 'vs/editor/common/view/viewEvents';
17
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
18 19
import { VerticalRevealType } from 'vs/editor/common/controller/cursorEvents';
import { ViewController } from 'vs/editor/browser/view/viewController';
A
Alex Dima 已提交
20
import { EndOfLinePreference } from "vs/editor/common/editorCommon";
21
import { IKeyboardEvent } from "vs/base/browser/keyboardEvent";
22 23 24
import { PartFingerprints, PartFingerprint } from "vs/editor/browser/view/viewPart";
import { Margin } from "vs/editor/browser/viewParts/margin/margin";
import { LineNumbersOverlay } from "vs/editor/browser/viewParts/lineNumbers/lineNumbers";
A
Alex Dima 已提交
25
import { BareFontInfo } from "vs/editor/common/config/fontInfo";
26

27
export interface ITextAreaHandlerHelper {
28 29
	visibleRangeForPositionRelativeToEditor(lineNumber: number, column: number): HorizontalRange;
	getVerticalOffsetForLineNumber(lineNumber: number): number;
30
}
31

32 33 34 35 36 37 38 39 40 41 42
class TextAreaVisiblePosition {
	_textAreaVisiblePosition: void;

	public readonly top: number;
	public readonly left: number;

	constructor(top: number, left: number) {
		this.top = top;
		this.left = left;
	}
}
43

44
export class TextAreaHandler extends ViewEventHandler {
45

46 47
	private readonly _context: ViewContext;
	private readonly _viewController: ViewController;
48
	private readonly _viewHelper: ITextAreaHandlerHelper;
49

50 51 52 53
	private _contentLeft: number;
	private _contentWidth: number;
	private _scrollLeft: number;
	private _scrollTop: number;
54

55
	private _visiblePosition: TextAreaVisiblePosition;
A
Alex Dima 已提交
56 57 58 59
	private _selections: Range[];
	private _lastCopiedValue: string;
	private _lastCopiedValueIsFromEmptySelection: boolean;

60 61
	public readonly textArea: FastDomNode<HTMLTextAreaElement>;
	public readonly textAreaCover: FastDomNode<HTMLElement>;
62
	private readonly _textAreaInput: TextAreaInput;
63

64
	constructor(context: ViewContext, viewController: ViewController, viewHelper: ITextAreaHandlerHelper) {
65
		super();
E
Erich Gamma 已提交
66

67
		this._context = context;
68 69
		this._viewController = viewController;
		this._viewHelper = viewHelper;
70

71 72 73 74
		this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
		this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
		this._scrollLeft = 0;
		this._scrollTop = 0;
75

76
		this._visiblePosition = null;
A
Alex Dima 已提交
77 78 79 80
		this._selections = [new Range(1, 1, 1, 1)];
		this._lastCopiedValue = null;
		this._lastCopiedValueIsFromEmptySelection = false;

81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
		// Text Area (The focus will always be in the textarea when the cursor is blinking)
		this.textArea = createFastDomNode(document.createElement('textarea'));
		PartFingerprints.write(this.textArea, PartFingerprint.TextArea);
		this.textArea.setClassName('inputarea');
		this.textArea.setAttribute('wrap', 'off');
		this.textArea.setAttribute('autocorrect', 'off');
		this.textArea.setAttribute('autocapitalize', 'off');
		this.textArea.setAttribute('spellcheck', 'false');
		this.textArea.setAttribute('aria-label', this._context.configuration.editor.viewInfo.ariaLabel);
		this.textArea.setAttribute('role', 'textbox');
		this.textArea.setAttribute('aria-multiline', 'true');
		this.textArea.setAttribute('aria-haspopup', 'false');
		this.textArea.setAttribute('aria-autocomplete', 'both');

		this.textArea.setTop(0);
		this.textArea.setLeft(0);
		Configuration.applyFontInfo(this.textArea, this._context.configuration.editor.fontInfo);

		// On top of the text area, we position a dom node to cover it up
		// (there have been reports of tiny blinking cursors)
		// (in WebKit the textarea is 1px by 1px because it cannot handle input to a 0x0 textarea)
		this.textAreaCover = createFastDomNode(document.createElement('div'));
		if (this._context.configuration.editor.viewInfo.glyphMargin) {
			this.textAreaCover.setClassName('monaco-editor-background ' + Margin.CLASS_NAME + ' ' + 'textAreaCover');
		} else {
			if (this._context.configuration.editor.viewInfo.renderLineNumbers) {
				this.textAreaCover.setClassName('monaco-editor-background ' + LineNumbersOverlay.CLASS_NAME + ' ' + 'textAreaCover');
			} else {
				this.textAreaCover.setClassName('monaco-editor-background ' + 'textAreaCover');
			}
		}
		this.textAreaCover.setPosition('absolute');
		this.textAreaCover.setWidth(1);
		this.textAreaCover.setHeight(1);
		this.textAreaCover.setTop(0);
		this.textAreaCover.setLeft(0);

118 119 120 121 122 123 124 125 126 127 128 129
		const simpleModel: ISimpleModel = {
			getLineCount: (): number => {
				return this._context.model.getLineCount();
			},
			getLineMaxColumn: (lineNumber: number): number => {
				return this._context.model.getLineMaxColumn(lineNumber);
			},
			getValueInRange: (range: Range, eol: EndOfLinePreference): string => {
				return this._context.model.getValueInRange(range, eol);
			}
		};

130
		const textAreaInputHost: ITextAreaInputHost = {
A
Alex Dima 已提交
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
			getPlainTextToCopy: (): string => {
				const whatToCopy = this._context.model.getPlainTextToCopy(this._selections, browser.enableEmptySelectionClipboard);

				if (browser.enableEmptySelectionClipboard) {
					if (browser.isFirefox) {
						// When writing "LINE\r\n" to the clipboard and then pasting,
						// Firefox pastes "LINE\n", so let's work around this quirk
						this._lastCopiedValue = whatToCopy.replace(/\r\n/g, '\n');
					} else {
						this._lastCopiedValue = whatToCopy;
					}

					let selections = this._selections;
					this._lastCopiedValueIsFromEmptySelection = (selections.length === 1 && selections[0].isEmpty());
				}

				return whatToCopy;
			},
149

A
Alex Dima 已提交
150 151 152
			getHTMLToCopy: (): string => {
				return this._context.model.getHTMLToCopy(this._selections, browser.enableEmptySelectionClipboard);
			},
153 154 155 156 157 158 159 160 161 162

			getScreenReaderContent: (currentState: TextAreaState): TextAreaState => {

				if (browser.isIPad) {
					// Do not place anything in the textarea for the iPad
					return TextAreaState.EMPTY;
				}

				const selection = this._selections[0];

A
Alex Dima 已提交
163 164
				if (this._context.configuration.editor.viewInfo.experimentalScreenReader) {
					return NVDAPagedStrategy.fromEditorSelection(currentState, simpleModel, selection);
165 166
				}

A
Alex Dima 已提交
167
				return IENarratorStrategy.fromEditorSelection(currentState, simpleModel, selection);
A
Alex Dima 已提交
168 169
			}
		};
170

171
		this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, this.textArea));
172 173 174 175 176 177 178 179 180 181

		this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => {
			this._viewController.emitKeyDown(e);
		}));

		this._register(this._textAreaInput.onKeyUp((e: IKeyboardEvent) => {
			this._viewController.emitKeyUp(e);
		}));

		this._register(this._textAreaInput.onPaste((e: IPasteData) => {
A
Alex Dima 已提交
182 183 184 185
			let pasteOnNewLine = false;
			if (browser.enableEmptySelectionClipboard) {
				pasteOnNewLine = (e.text === this._lastCopiedValue && this._lastCopiedValueIsFromEmptySelection);
			}
186 187 188 189 190
			this._viewController.paste('keyboard', e.text, pasteOnNewLine);
		}));

		this._register(this._textAreaInput.onCut(() => {
			this._viewController.cut('keyboard');
A
Alex Dima 已提交
191
		}));
192 193

		this._register(this._textAreaInput.onType((e: ITypeData) => {
194
			if (e.replaceCharCnt) {
195
				this._viewController.replacePreviousChar('keyboard', e.text, e.replaceCharCnt);
196
			} else {
197
				this._viewController.type('keyboard', e.text);
198 199
			}
		}));
200

201
		this._register(this._textAreaInput.onCompositionStart(() => {
A
Alex Dima 已提交
202 203
			const lineNumber = this._selections[0].startLineNumber;
			const column = this._selections[0].startColumn;
204

205
			this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
A
Alex Dima 已提交
206
				new Range(lineNumber, column, lineNumber, column),
207
				VerticalRevealType.Simple,
208
				true
A
Alex Dima 已提交
209
			));
210 211

			// Find range pixel position
212
			const visibleRange = this._viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column);
213 214

			if (visibleRange) {
215 216
				this._visiblePosition = new TextAreaVisiblePosition(
					this._viewHelper.getVerticalOffsetForLineNumber(lineNumber),
217 218
					visibleRange.left
				);
219 220
				this.textArea.setTop(this._visiblePosition.top - this._scrollTop);
				this.textArea.setLeft(this._contentLeft + this._visiblePosition.left - this._scrollLeft);
221 222 223
			}

			// Show the textarea
224
			this.textArea.setHeight(this._context.configuration.editor.lineHeight);
225
			this.textArea.setClassName('inputarea ime-input');
226

227
			this._viewController.compositionStart('keyboard');
228
		}));
229

230
		this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => {
231 232 233
			if (browser.isEdgeOrIE) {
				// Due to isEdgeOrIE (where the textarea was not cleared initially)
				// we cannot assume the text consists only of the composited text
234
				this.textArea.setWidth(0);
235 236
			} else {
				// adjust width by its size
A
Alex Dima 已提交
237
				this.textArea.setWidth(measureText(e.data, this._context.configuration.editor.fontInfo));
238
			}
239 240
		}));

241
		this._register(this._textAreaInput.onCompositionEnd(() => {
242 243 244 245
			this.textArea.unsetHeight();
			this.textArea.unsetWidth();
			this.textArea.setLeft(0);
			this.textArea.setTop(0);
246
			this.textArea.setClassName('inputarea');
247

248
			this._visiblePosition = null;
249

250
			this._viewController.compositionEnd('keyboard');
251
		}));
252

253 254 255 256 257 258 259 260
		this._register(this._textAreaInput.onFocus(() => {
			this._context.privateViewEventBus.emit(new viewEvents.ViewFocusChangedEvent(true));
		}));

		this._register(this._textAreaInput.onBlur(() => {
			this._context.privateViewEventBus.emit(new viewEvents.ViewFocusChangedEvent(false));
		}));

261
		this._context.addEventHandler(this);
E
Erich Gamma 已提交
262 263
	}

264
	public dispose(): void {
265
		this._context.removeEventHandler(this);
A
Alex Dima 已提交
266
		super.dispose();
267 268
	}

269 270
	// --- begin event handlers

A
Alex Dima 已提交
271
	public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
272
		// Give textarea same font size & line height as editor, for the IME case (when the textarea is visible)
273
		if (e.fontInfo) {
274
			Configuration.applyFontInfo(this.textArea, this._context.configuration.editor.fontInfo);
275
		}
276
		if (e.viewInfo.experimentalScreenReader) {
277
			this._textAreaInput.writeScreenReaderContent('strategy changed');
278
		}
A
Alex Dima 已提交
279
		if (e.layoutInfo) {
280 281
			this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
			this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
A
Alex Dima 已提交
282
		}
283
		if (e.viewInfo.ariaLabel) {
284
			this.textArea.setAttribute('aria-label', this._context.configuration.editor.viewInfo.ariaLabel);
285
		}
286 287 288
		return false;
	}

289
	public onCursorSelectionChanged(e: viewEvents.ViewCursorSelectionChangedEvent): boolean {
A
Alex Dima 已提交
290
		this._selections = [e.selection].concat(e.secondarySelections);
291 292 293
		return false;
	}

294
	public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
295 296 297
		this._scrollLeft = e.scrollLeft;
		this._scrollTop = e.scrollTop;
		if (this._visiblePosition) {
298 299
			this.textArea.setTop(this._visiblePosition.top - this._scrollTop);
			this.textArea.setLeft(this._contentLeft + this._visiblePosition.left - this._scrollLeft);
300
		}
301 302 303
		return false;
	}

304 305
	// --- end event handlers

306 307
	// --- begin view API

A
Alex Dima 已提交
308 309 310 311 312 313 314 315
	public isFocused(): boolean {
		return this._textAreaInput.isFocused();
	}

	public focusTextArea(): void {
		this._textAreaInput.focusTextArea();
	}

316
	public writeToTextArea(): void {
317
		this._textAreaInput.writeScreenReaderContent('selection changed');
318
	}
319 320 321

	public setAriaActiveDescendant(id: string): void {
		if (id) {
322 323 324 325
			this.textArea.setAttribute('role', 'combobox');
			if (this.textArea.getAttribute('aria-activedescendant') !== id) {
				this.textArea.setAttribute('aria-haspopup', 'true');
				this.textArea.setAttribute('aria-activedescendant', id);
326 327
			}
		} else {
328 329 330
			this.textArea.setAttribute('role', 'textbox');
			this.textArea.removeAttribute('aria-activedescendant');
			this.textArea.removeAttribute('aria-haspopup');
331 332 333 334
		}
	}

	// --- end view API
335
}
A
Alex Dima 已提交
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361

function measureText(text: string, fontInfo: BareFontInfo): number {
	// adjust width by its size
	const canvasElem = <HTMLCanvasElement>document.createElement('canvas');
	const context = canvasElem.getContext('2d');
	context.font = createFontString(fontInfo);
	const metrics = context.measureText(text);

	if (browser.isFirefox) {
		return metrics.width + 2; // +2 for Japanese...
	} else {
		return metrics.width;
	}
}

function createFontString(bareFontInfo: BareFontInfo): string {
	return doCreateFontString('normal', bareFontInfo.fontWeight, bareFontInfo.fontSize, bareFontInfo.lineHeight, bareFontInfo.fontFamily);
}

function doCreateFontString(fontStyle: string, fontWeight: string, fontSize: number, lineHeight: number, fontFamily: string): string {
	// The full font syntax is:
	// style | variant | weight | stretch | size/line-height | fontFamily
	// (https://developer.mozilla.org/en-US/docs/Web/CSS/font)
	// But it appears Edge and IE11 cannot properly parse `stretch`.
	return `${fontStyle} normal ${fontWeight} ${fontSize}px / ${lineHeight}px ${fontFamily}`;
}