textAreaHandler.ts 13.1 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';

A
Alex Dima 已提交
7 8
import * as browser from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
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";
25

26
export interface ITextAreaHandlerHelper {
A
Alex Dima 已提交
27
	viewDomNode: FastDomNode<HTMLElement>;
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 45 46 47 48

export const enum TextAreaStrategy {
	IENarrator,
	NVDA
}

49
export class TextAreaHandler extends ViewEventHandler {
50

51 52
	private readonly _context: ViewContext;
	private readonly _viewController: ViewController;
53
	private readonly _viewHelper: ITextAreaHandlerHelper;
54

55 56 57 58
	private _contentLeft: number;
	private _contentWidth: number;
	private _scrollLeft: number;
	private _scrollTop: number;
59

60
	private _visiblePosition: TextAreaVisiblePosition;
A
Alex Dima 已提交
61 62 63 64
	private _selections: Range[];
	private _lastCopiedValue: string;
	private _lastCopiedValueIsFromEmptySelection: boolean;

65 66
	public readonly textArea: FastDomNode<HTMLTextAreaElement>;
	public readonly textAreaCover: FastDomNode<HTMLElement>;
67
	private readonly _textAreaInput: TextAreaInput;
68

69
	constructor(context: ViewContext, viewController: ViewController, viewHelper: ITextAreaHandlerHelper) {
70
		super();
E
Erich Gamma 已提交
71

72
		this._context = context;
73 74
		this._viewController = viewController;
		this._viewHelper = viewHelper;
75

76 77 78 79
		this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
		this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
		this._scrollLeft = 0;
		this._scrollTop = 0;
80

81
		this._visiblePosition = null;
A
Alex Dima 已提交
82 83 84 85
		this._selections = [new Range(1, 1, 1, 1)];
		this._lastCopiedValue = null;
		this._lastCopiedValueIsFromEmptySelection = false;

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 118 119 120 121 122
		// 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);

123 124 125 126 127 128 129 130 131 132 133 134
		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);
			}
		};

135
		const textAreaInputHost: ITextAreaInputHost = {
A
Alex Dima 已提交
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
			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;
			},
154

A
Alex Dima 已提交
155 156 157
			getHTMLToCopy: (): string => {
				return this._context.model.getHTMLToCopy(this._selections, browser.enableEmptySelectionClipboard);
			},
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173

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

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

				const strategy = this._getStrategy();
				const selection = this._selections[0];

				if (strategy === TextAreaStrategy.IENarrator) {
					return IENarratorStrategy.fromEditorSelection(currentState, simpleModel, selection);
				}

				return NVDAPagedStrategy.fromEditorSelection(currentState, simpleModel, selection);
A
Alex Dima 已提交
174 175
			}
		};
176

177
		this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, this.textArea));
178 179 180 181 182 183 184 185 186 187

		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 已提交
188 189 190 191
			let pasteOnNewLine = false;
			if (browser.enableEmptySelectionClipboard) {
				pasteOnNewLine = (e.text === this._lastCopiedValue && this._lastCopiedValueIsFromEmptySelection);
			}
192 193 194 195 196
			this._viewController.paste('keyboard', e.text, pasteOnNewLine);
		}));

		this._register(this._textAreaInput.onCut(() => {
			this._viewController.cut('keyboard');
A
Alex Dima 已提交
197
		}));
198 199

		this._register(this._textAreaInput.onType((e: ITypeData) => {
200
			if (e.replaceCharCnt) {
201
				this._viewController.replacePreviousChar('keyboard', e.text, e.replaceCharCnt);
202
			} else {
203
				this._viewController.type('keyboard', e.text);
204 205
			}
		}));
206

207
		this._register(this._textAreaInput.onCompositionStart(() => {
A
Alex Dima 已提交
208 209
			const lineNumber = this._selections[0].startLineNumber;
			const column = this._selections[0].startColumn;
210

211
			this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
A
Alex Dima 已提交
212
				new Range(lineNumber, column, lineNumber, column),
213
				VerticalRevealType.Simple,
214
				true
A
Alex Dima 已提交
215
			));
216 217

			// Find range pixel position
218
			const visibleRange = this._viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column);
219 220

			if (visibleRange) {
221 222
				this._visiblePosition = new TextAreaVisiblePosition(
					this._viewHelper.getVerticalOffsetForLineNumber(lineNumber),
223 224
					visibleRange.left
				);
225 226
				this.textArea.setTop(this._visiblePosition.top - this._scrollTop);
				this.textArea.setLeft(this._contentLeft + this._visiblePosition.left - this._scrollLeft);
227 228 229
			}

			// Show the textarea
230
			this.textArea.setHeight(this._context.configuration.editor.lineHeight);
231
			this._viewHelper.viewDomNode.addClassName('ime-input');
232

233
			this._viewController.compositionStart('keyboard');
234
		}));
235

236
		this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => {
237 238 239
			if (browser.isEdgeOrIE) {
				// Due to isEdgeOrIE (where the textarea was not cleared initially)
				// we cannot assume the text consists only of the composited text
240
				this.textArea.setWidth(0);
241 242 243 244
			} else {
				// adjust width by its size
				let canvasElem = <HTMLCanvasElement>document.createElement('canvas');
				let context = canvasElem.getContext('2d');
245
				let cs = dom.getComputedStyle(this.textArea.domNode);
A
Alex Dima 已提交
246 247
				if (browser.isFirefox) {
					// computedStyle.font is empty in Firefox...
248 249
					context.font = `${cs.fontStyle} ${cs.fontVariant} ${cs.fontWeight} ${cs.fontStretch} ${cs.fontSize} / ${cs.lineHeight} ${cs.fontFamily}`;
					let metrics = context.measureText(e.data);
250
					this.textArea.setWidth(metrics.width + 2); // +2 for Japanese...
A
Alex Dima 已提交
251 252
				} else {
					context.font = cs.font;
253
					let metrics = context.measureText(e.data);
254
					this.textArea.setWidth(metrics.width);
A
Alex Dima 已提交
255
				}
256
			}
257 258
		}));

259
		this._register(this._textAreaInput.onCompositionEnd(() => {
260 261 262 263
			this.textArea.unsetHeight();
			this.textArea.unsetWidth();
			this.textArea.setLeft(0);
			this.textArea.setTop(0);
264
			this._viewHelper.viewDomNode.removeClassName('ime-input');
265

266
			this._visiblePosition = null;
267

268
			this._viewController.compositionEnd('keyboard');
269
		}));
270

271 272 273 274 275 276 277 278
		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));
		}));

279
		this._context.addEventHandler(this);
E
Erich Gamma 已提交
280 281
	}

282
	public dispose(): void {
283
		this._context.removeEventHandler(this);
A
Alex Dima 已提交
284
		super.dispose();
285 286
	}

287
	private _getStrategy(): TextAreaStrategy {
288
		if (this._context.configuration.editor.viewInfo.experimentalScreenReader) {
289 290 291 292 293
			return TextAreaStrategy.NVDA;
		}
		return TextAreaStrategy.IENarrator;
	}

294 295 296 297
	public isFocused(): boolean {
		return this._textAreaInput.isFocused();
	}

298
	public focusTextArea(): void {
299
		this._textAreaInput.focusTextArea();
300 301
	}

302 303
	// --- begin event handlers

A
Alex Dima 已提交
304
	public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
305
		// Give textarea same font size & line height as editor, for the IME case (when the textarea is visible)
306
		if (e.fontInfo) {
307
			Configuration.applyFontInfo(this.textArea, this._context.configuration.editor.fontInfo);
308
		}
309
		if (e.viewInfo.experimentalScreenReader) {
310
			this._textAreaInput.writeScreenReaderContent('strategy changed');
311
		}
A
Alex Dima 已提交
312
		if (e.layoutInfo) {
313 314
			this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
			this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
A
Alex Dima 已提交
315
		}
316
		if (e.viewInfo.ariaLabel) {
317
			this.textArea.setAttribute('aria-label', this._context.configuration.editor.viewInfo.ariaLabel);
318
		}
319 320 321
		return false;
	}

322
	public onCursorSelectionChanged(e: viewEvents.ViewCursorSelectionChangedEvent): boolean {
A
Alex Dima 已提交
323
		this._selections = [e.selection].concat(e.secondarySelections);
324 325 326
		return false;
	}

327
	public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
328 329 330
		this._scrollLeft = e.scrollLeft;
		this._scrollTop = e.scrollTop;
		if (this._visiblePosition) {
331 332
			this.textArea.setTop(this._visiblePosition.top - this._scrollTop);
			this.textArea.setLeft(this._contentLeft + this._visiblePosition.left - this._scrollLeft);
333
		}
334 335 336
		return false;
	}

337 338
	// --- end event handlers

339 340
	// --- begin view API

341
	public writeToTextArea(): void {
342
		this._textAreaInput.writeScreenReaderContent('selection changed');
343
	}
344 345 346

	public setAriaActiveDescendant(id: string): void {
		if (id) {
347 348 349 350
			this.textArea.setAttribute('role', 'combobox');
			if (this.textArea.getAttribute('aria-activedescendant') !== id) {
				this.textArea.setAttribute('aria-haspopup', 'true');
				this.textArea.setAttribute('aria-activedescendant', id);
351 352
			}
		} else {
353 354 355
			this.textArea.setAttribute('role', 'textbox');
			this.textArea.removeAttribute('aria-activedescendant');
			this.textArea.removeAttribute('aria-haspopup');
356 357 358 359
		}
	}

	// --- end view API
360
}