keyboardHandler.ts 12.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
import Event, {Emitter} from 'vs/base/common/event';
A
Alex Dima 已提交
8 9 10
import {Disposable, IDisposable, disposeAll} from 'vs/base/common/lifecycle';
import * as browser from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
A
Cleanup  
Alex Dima 已提交
11
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
A
Alex Dima 已提交
12 13 14 15 16 17 18 19
import {StyleMutator} from 'vs/base/browser/styleMutator';
import {GlobalScreenReaderNVDA} from 'vs/editor/common/config/commonEditorConfig';
import {TextAreaHandler} from 'vs/editor/common/controller/textAreaHandler';
import {IClipboardEvent, IKeyboardEventWrapper, ITextAreaWrapper, TextAreaStrategy} from 'vs/editor/common/controller/textAreaState';
import {Range} from 'vs/editor/common/core/range';
import * as editorCommon from 'vs/editor/common/editorCommon';
import {ViewEventHandler} from 'vs/editor/common/viewModel/viewEventHandler';
import {IKeyboardHandlerHelper, IViewContext, IViewController} from 'vs/editor/browser/editorBrowser';
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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 64 65 66 67 68 69 70 71

class ClipboardEventWrapper implements IClipboardEvent {

	private _event:ClipboardEvent;

	constructor(event:ClipboardEvent) {
		this._event = event;
	}

	public canUseTextData(): boolean {
		if (this._event.clipboardData) {
			return true;
		}
		if ((<any>window).clipboardData) {
			return true;
		}
		return false;
	}

	public setTextData(text:string): void {
		if (this._event.clipboardData) {
			this._event.clipboardData.setData('text/plain', text);
			this._event.preventDefault();
			return;
		}

		if ((<any>window).clipboardData) {
			(<any>window).clipboardData.setData('Text', text);
			this._event.preventDefault();
			return;
		}

		throw new Error('ClipboardEventWrapper.setTextData: Cannot use text data!');
	}

	public getTextData(): string {
		if (this._event.clipboardData) {
			this._event.preventDefault();
			return this._event.clipboardData.getData('text/plain');
		}

		if ((<any>window).clipboardData) {
			this._event.preventDefault();
			return (<any>window).clipboardData.getData('Text');
		}

		throw new Error('ClipboardEventWrapper.getTextData: Cannot use text data!');
	}
}

class KeyboardEventWrapper implements IKeyboardEventWrapper {

A
Cleanup  
Alex Dima 已提交
72
	public _actual: IKeyboardEvent;
73

A
Cleanup  
Alex Dima 已提交
74
	constructor(actual:IKeyboardEvent) {
75
		this._actual = actual;
76 77
	}

78 79 80 81 82 83 84 85 86 87 88 89 90 91
	public equals(keybinding:number): boolean {
		return this._actual.equals(keybinding);
	}

	public preventDefault(): void {
		this._actual.preventDefault();
	}

	public isDefaultPrevented(): boolean {
		if (this._actual.browserEvent) {
			return this._actual.browserEvent.defaultPrevented;
		}
		return false;
	}
92
}
E
Erich Gamma 已提交
93

A
Alex Dima 已提交
94
class TextAreaWrapper extends Disposable implements ITextAreaWrapper {
A
Alex Dima 已提交
95 96 97

	private _textArea: HTMLTextAreaElement;

98 99
	private _onKeyDown = this._register(new Emitter<IKeyboardEventWrapper>());
	public onKeyDown: Event<IKeyboardEventWrapper> = this._onKeyDown.event;
A
Alex Dima 已提交
100

101 102
	private _onKeyUp = this._register(new Emitter<IKeyboardEventWrapper>());
	public onKeyUp: Event<IKeyboardEventWrapper> = this._onKeyUp.event;
A
Alex Dima 已提交
103

104 105
	private _onKeyPress = this._register(new Emitter<IKeyboardEventWrapper>());
	public onKeyPress: Event<IKeyboardEventWrapper> = this._onKeyPress.event;
A
Alex Dima 已提交
106 107 108 109 110 111 112 113 114 115

	private _onCompositionStart = this._register(new Emitter<void>());
	public onCompositionStart: Event<void> = this._onCompositionStart.event;

	private _onCompositionEnd = this._register(new Emitter<void>());
	public onCompositionEnd: Event<void> = this._onCompositionEnd.event;

	private _onInput = this._register(new Emitter<void>());
	public onInput: Event<void> = this._onInput.event;

116 117
	private _onCut = this._register(new Emitter<IClipboardEvent>());
	public onCut: Event<IClipboardEvent> = this._onCut.event;
A
Alex Dima 已提交
118

119 120
	private _onCopy = this._register(new Emitter<IClipboardEvent>());
	public onCopy: Event<IClipboardEvent> = this._onCopy.event;
A
Alex Dima 已提交
121

122 123
	private _onPaste = this._register(new Emitter<IClipboardEvent>());
	public onPaste: Event<IClipboardEvent> = this._onPaste.event;
A
Alex Dima 已提交
124 125 126 127 128

	constructor(textArea: HTMLTextAreaElement) {
		super();
		this._textArea = textArea;

A
Alex Dima 已提交
129 130 131 132 133 134 135 136 137
		this._register(dom.addStandardDisposableListener(this._textArea, 'keydown', (e) => this._onKeyDown.fire(new KeyboardEventWrapper(e))));
		this._register(dom.addStandardDisposableListener(this._textArea, 'keyup', (e) => this._onKeyUp.fire(new KeyboardEventWrapper(e))));
		this._register(dom.addStandardDisposableListener(this._textArea, 'keypress', (e) => this._onKeyPress.fire(new KeyboardEventWrapper(e))));
		this._register(dom.addDisposableListener(this._textArea, 'compositionstart', (e) => this._onCompositionStart.fire()));
		this._register(dom.addDisposableListener(this._textArea, 'compositionend', (e) => this._onCompositionEnd.fire()));
		this._register(dom.addDisposableListener(this._textArea, 'input', (e) => this._onInput.fire()));
		this._register(dom.addDisposableListener(this._textArea, 'cut', (e:ClipboardEvent) => this._onCut.fire(new ClipboardEventWrapper(e))));
		this._register(dom.addDisposableListener(this._textArea, 'copy', (e:ClipboardEvent) => this._onCopy.fire(new ClipboardEventWrapper(e))));
		this._register(dom.addDisposableListener(this._textArea, 'paste', (e:ClipboardEvent) => this._onPaste.fire(new ClipboardEventWrapper(e))));
A
Alex Dima 已提交
138 139
	}

A
Alex Dima 已提交
140 141 142 143
	public get actual(): HTMLTextAreaElement {
		return this._textArea;
	}

144 145
	public getValue(): string {
		// console.log('current value: ' + this._textArea.value);
A
Alex Dima 已提交
146 147 148
		return this._textArea.value;
	}

149 150
	public setValue(reason:string, value:string): void {
		// console.log('reason: ' + reason + ', current value: ' + this._textArea.value + ' => new value: ' + value);
A
Alex Dima 已提交
151 152 153
		this._textArea.value = value;
	}

154
	public getSelectionStart(): number {
A
Alex Dima 已提交
155 156 157
		return this._textArea.selectionStart;
	}

158
	public getSelectionEnd(): number {
A
Alex Dima 已提交
159 160 161 162
		return this._textArea.selectionEnd;
	}

	public setSelectionRange(selectionStart:number, selectionEnd:number): void {
163 164 165 166 167 168 169 170 171
		let activeElement = document.activeElement;
		if (activeElement === this._textArea) {
			this._textArea.setSelectionRange(selectionStart, selectionEnd);
		} else {
			this._setSelectionRangeJumpy(selectionStart, selectionEnd);
		}
	}

	private _setSelectionRangeJumpy(selectionStart:number, selectionEnd:number): void {
A
Alex Dima 已提交
172
		try {
A
Alex Dima 已提交
173
			let scrollState = dom.saveParentsScrollTop(this._textArea);
A
Alex Dima 已提交
174
			this._textArea.focus();
175
			this._textArea.setSelectionRange(selectionStart, selectionEnd);
A
Alex Dima 已提交
176
			dom.restoreParentsScrollTop(this._textArea, scrollState);
A
Alex Dima 已提交
177 178
		} catch(e) {
			// Sometimes IE throws when setting selection (e.g. textarea is off-DOM)
179
			console.log('an error has been thrown!');
A
Alex Dima 已提交
180 181 182
		}
	}

183 184
	public isInOverwriteMode(): boolean {
		// In IE, pressing Insert will bring the typing into overwrite mode
A
Alex Dima 已提交
185
		if (browser.isIE11orEarlier && document.queryCommandValue('OverWrite')) {
186 187 188 189
			return true;
		}
		return false;
	}
A
Alex Dima 已提交
190 191
}

192

A
Alex Dima 已提交
193
export class KeyboardHandler extends ViewEventHandler implements IDisposable {
194

A
Alex Dima 已提交
195 196 197
	private context:IViewContext;
	private viewController:IViewController;
	private viewHelper:IKeyboardHandlerHelper;
198 199
	private textArea:TextAreaWrapper;
	private textAreaHandler:TextAreaHandler;
A
Alex Dima 已提交
200
	private _toDispose:IDisposable[];
201 202 203 204

	private contentLeft:number;
	private contentWidth:number;
	private scrollLeft:number;
205

A
Alex Dima 已提交
206
	constructor(context:IViewContext, viewController:IViewController, viewHelper:IKeyboardHandlerHelper) {
207
		super();
E
Erich Gamma 已提交
208

209 210 211 212 213
		this.context = context;
		this.viewController = viewController;
		this.textArea = new TextAreaWrapper(viewHelper.textArea);
		this.viewHelper = viewHelper;

214 215 216 217
		this.contentLeft = 0;
		this.contentWidth = 0;
		this.scrollLeft = 0;

218
		this.textAreaHandler = new TextAreaHandler(browser, this._getStrategy(), this.textArea, this.context.model, () => this.viewHelper.flushAnyAccumulatedEvents());
219

220
		this._toDispose = [];
A
Cleanup  
Alex Dima 已提交
221 222
		this._toDispose.push(this.textAreaHandler.onKeyDown((e) => this.viewController.emitKeyDown(<IKeyboardEvent>e._actual)));
		this._toDispose.push(this.textAreaHandler.onKeyUp((e) => this.viewController.emitKeyUp(<IKeyboardEvent>e._actual)));
223 224 225
		this._toDispose.push(this.textAreaHandler.onPaste((e) => this.viewController.paste('keyboard', e.text, e.pasteOnNewLine)));
		this._toDispose.push(this.textAreaHandler.onCut((e) => this.viewController.cut('keyboard')));
		this._toDispose.push(this.textAreaHandler.onType((e) => {
226 227
			if (e.replaceCharCnt) {
				this.viewController.replacePreviousChar('keyboard', e.text, e.replaceCharCnt);
228 229 230 231 232 233 234 235
			} else {
				this.viewController.type('keyboard', e.text);
			}
		}));
		this._toDispose.push(this.textAreaHandler.onCompositionStart((e) => {
			let lineNumber = e.showAtLineNumber;
			let column = e.showAtColumn;

A
Alex Dima 已提交
236
			let revealPositionEvent:editorCommon.IViewRevealRangeEvent = {
237
				range: new Range(lineNumber, column, lineNumber, column),
A
Alex Dima 已提交
238
				verticalType: editorCommon.VerticalRevealType.Simple,
239 240
				revealHorizontal: true
			};
A
Alex Dima 已提交
241
			this.context.privateViewEventBus.emit(editorCommon.ViewEventNames.RevealRangeEvent, revealPositionEvent);
242 243 244 245 246

			// Find range pixel position
			let visibleRange = this.viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column);

			if (visibleRange) {
A
Alex Dima 已提交
247 248
				StyleMutator.setTop(this.textArea.actual, visibleRange.top);
				StyleMutator.setLeft(this.textArea.actual, this.contentLeft + visibleRange.left - this.scrollLeft);
249 250
			}

A
Alex Dima 已提交
251
			if (browser.isIE11orEarlier) {
A
Alex Dima 已提交
252
				StyleMutator.setWidth(this.textArea.actual, this.contentWidth);
253 254 255
			}

			// Show the textarea
A
Alex Dima 已提交
256
			StyleMutator.setHeight(this.textArea.actual, this.context.configuration.editor.lineHeight);
A
Alex Dima 已提交
257
			dom.addClass(this.viewHelper.viewDomNode, 'ime-input');
258 259
		}));
		this._toDispose.push(this.textAreaHandler.onCompositionEnd((e) => {
A
Alex Dima 已提交
260 261
			this.textArea.actual.style.height = '';
			this.textArea.actual.style.width = '';
A
Alex Dima 已提交
262 263
			StyleMutator.setLeft(this.textArea.actual, 0);
			StyleMutator.setTop(this.textArea.actual, 0);
A
Alex Dima 已提交
264
			dom.removeClass(this.viewHelper.viewDomNode, 'ime-input');
265
		}));
266 267 268
		this._toDispose.push(GlobalScreenReaderNVDA.onChange((value) => {
			this.textAreaHandler.setStrategy(this._getStrategy());
		}));
269 270


271
		this.context.addEventHandler(this);
E
Erich Gamma 已提交
272 273
	}

274 275 276 277
	public dispose(): void {
		this.context.removeEventHandler(this);
		this.textAreaHandler.dispose();
		this.textArea.dispose();
A
Alex Dima 已提交
278
		this._toDispose = disposeAll(this._toDispose);
279 280
	}

281
	private _getStrategy(): TextAreaStrategy {
282 283 284 285
		if (GlobalScreenReaderNVDA.getValue()) {
			return TextAreaStrategy.NVDA;
		}
		if (this.context.configuration.editor.experimentalScreenReader) {
286 287 288 289 290
			return TextAreaStrategy.NVDA;
		}
		return TextAreaStrategy.IENarrator;
	}

291 292 293 294
	public focusTextArea(): void {
		this.textAreaHandler.writePlaceholderAndSelectTextAreaSync();
	}

A
Alex Dima 已提交
295
	public onConfigurationChanged(e: editorCommon.IConfigurationChangedEvent): boolean {
296
		// Give textarea same font size & line height as editor, for the IME case (when the textarea is visible)
A
Alex Dima 已提交
297 298
		StyleMutator.setFontSize(this.textArea.actual, this.context.configuration.editor.fontSize);
		StyleMutator.setLineHeight(this.textArea.actual, this.context.configuration.editor.lineHeight);
299
		if (e.experimentalScreenReader) {
300 301
			this.textAreaHandler.setStrategy(this._getStrategy());
		}
302 303 304
		return false;
	}

A
Alex Dima 已提交
305
	public onScrollChanged(e:editorCommon.IScrollEvent): boolean {
306
		this.scrollLeft = e.scrollLeft;
307 308 309 310
		return false;
	}

	public onViewFocusChanged(isFocused:boolean): boolean {
311
		this.textAreaHandler.setHasFocus(isFocused);
312 313 314
		return false;
	}

315
	private _lastCursorSelectionChanged:editorCommon.IViewCursorSelectionChangedEvent = null;
A
Alex Dima 已提交
316
	public onCursorSelectionChanged(e:editorCommon.IViewCursorSelectionChangedEvent): boolean {
317
		this._lastCursorSelectionChanged = e;
318 319 320
		return false;
	}

A
Alex Dima 已提交
321
	public onCursorPositionChanged(e:editorCommon.IViewCursorPositionChangedEvent): boolean {
322
		this.textAreaHandler.setCursorPosition(e.position);
323 324 325
		return false;
	}

A
Alex Dima 已提交
326
	public onLayoutChanged(layoutInfo:editorCommon.IEditorLayoutInfo): boolean {
327 328
		this.contentLeft = layoutInfo.contentLeft;
		this.contentWidth = layoutInfo.contentWidth;
329 330
		return false;
	}
E
Erich Gamma 已提交
331

332 333 334 335 336 337 338 339
	public writeToTextArea(): void {
		if (this._lastCursorSelectionChanged) {
			let e = this._lastCursorSelectionChanged;
			this._lastCursorSelectionChanged = null;
			this.textAreaHandler.setCursorSelections(e.selection, e.secondarySelections);
		}
	}

340
}