Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts
5240 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as browser from '../../../../../base/browser/browser.js';
7
import * as dom from '../../../../../base/browser/dom.js';
8
import { DomEmitter } from '../../../../../base/browser/event.js';
9
import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
10
import { inputLatency } from '../../../../../base/browser/performance.js';
11
import { RunOnceScheduler } from '../../../../../base/common/async.js';
12
import { Emitter, Event } from '../../../../../base/common/event.js';
13
import { KeyCode } from '../../../../../base/common/keyCodes.js';
14
import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
15
import { OperatingSystem } from '../../../../../base/common/platform.js';
16
import * as strings from '../../../../../base/common/strings.js';
17
import { Position } from '../../../../common/core/position.js';
18
import { Selection } from '../../../../common/core/selection.js';
19
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
20
import { ILogService } from '../../../../../platform/log/common/log.js';
21
import { ClipboardStoredMetadata, CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../clipboardUtils.js';
22
import { _debugComposition, ITextAreaWrapper, ITypeData, TextAreaState } from './textAreaEditContextState.js';
23
import { ViewContext } from '../../../../common/viewModel/viewContext.js';
24
25
export namespace TextAreaSyntethicEvents {
26
export const Tap = '-monaco-textarea-synthetic-tap';
27
}
28
29
export interface ICompositionData {
30
data: string;
31
}
32
33
34
export interface IPasteData {
35
text: string;
36
metadata: ClipboardStoredMetadata | null;
37
}
38
39
export interface ITextAreaInputHost {
40
readonly context: ViewContext;
41
getScreenReaderContent(): TextAreaState;
42
deduceModelPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position;
43
}
44
45
export interface ICompositionStartEvent {
46
data: string;
47
}
48
49
export interface ICompleteTextAreaWrapper extends ITextAreaWrapper {
50
readonly onKeyDown: Event<KeyboardEvent>;
51
readonly onKeyPress: Event<KeyboardEvent>;
52
readonly onKeyUp: Event<KeyboardEvent>;
53
readonly onCompositionStart: Event<CompositionEvent>;
54
readonly onCompositionUpdate: Event<CompositionEvent>;
55
readonly onCompositionEnd: Event<CompositionEvent>;
56
readonly onBeforeInput: Event<InputEvent>;
57
readonly onInput: Event<InputEvent>;
58
readonly onCut: Event<ClipboardEvent>;
59
readonly onCopy: Event<ClipboardEvent>;
60
readonly onPaste: Event<ClipboardEvent>;
61
readonly onFocus: Event<FocusEvent>;
62
readonly onBlur: Event<FocusEvent>;
63
readonly onSyntheticTap: Event<void>;
64
65
readonly ownerDocument: Document;
66
67
setIgnoreSelectionChangeTime(reason: string): void;
68
getIgnoreSelectionChangeTime(): number;
69
resetSelectionChangeTime(): void;
70
71
hasFocus(): boolean;
72
}
73
74
export interface IBrowser {
75
isAndroid: boolean;
76
isFirefox: boolean;
77
isChrome: boolean;
78
isSafari: boolean;
79
}
80
81
class CompositionContext {
82
83
private _lastTypeTextLength: number;
84
85
constructor() {
86
this._lastTypeTextLength = 0;
87
}
88
89
public handleCompositionUpdate(text: string | null | undefined): ITypeData {
90
text = text || '';
91
const typeInput: ITypeData = {
92
text: text,
93
replacePrevCharCnt: this._lastTypeTextLength,
94
replaceNextCharCnt: 0,
95
positionDelta: 0
96
};
97
this._lastTypeTextLength = text.length;
98
return typeInput;
99
}
100
}
101
102
/**
103
* Writes screen reader content to the textarea and is able to analyze its input events to generate:
104
* - onCut
105
* - onPaste
106
* - onType
107
*
108
* Composition events are generated for presentation purposes (composition input is reflected in onType).
109
*/
110
export class TextAreaInput extends Disposable {
111
112
private _onFocus = this._register(new Emitter<void>());
113
public readonly onFocus: Event<void> = this._onFocus.event;
114
115
private _onBlur = this._register(new Emitter<void>());
116
public readonly onBlur: Event<void> = this._onBlur.event;
117
118
private _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
119
public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
120
121
private _onKeyUp = this._register(new Emitter<IKeyboardEvent>());
122
public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;
123
124
private _onCut = this._register(new Emitter<void>());
125
public readonly onCut: Event<void> = this._onCut.event;
126
127
private _onPaste = this._register(new Emitter<IPasteData>());
128
public readonly onPaste: Event<IPasteData> = this._onPaste.event;
129
130
private _onWillCopy = this._register(new Emitter<IClipboardCopyEvent>());
131
public readonly onWillCopy: Event<IClipboardCopyEvent> = this._onWillCopy.event;
132
133
private _onWillCut = this._register(new Emitter<IClipboardCopyEvent>());
134
public readonly onWillCut: Event<IClipboardCopyEvent> = this._onWillCut.event;
135
136
private _onWillPaste = this._register(new Emitter<IClipboardPasteEvent>());
137
public readonly onWillPaste: Event<IClipboardPasteEvent> = this._onWillPaste.event;
138
139
private _onType = this._register(new Emitter<ITypeData>());
140
public readonly onType: Event<ITypeData> = this._onType.event;
141
142
private _onCompositionStart = this._register(new Emitter<ICompositionStartEvent>());
143
public readonly onCompositionStart: Event<ICompositionStartEvent> = this._onCompositionStart.event;
144
145
private _onCompositionUpdate = this._register(new Emitter<ICompositionData>());
146
public readonly onCompositionUpdate: Event<ICompositionData> = this._onCompositionUpdate.event;
147
148
private _onCompositionEnd = this._register(new Emitter<void>());
149
public readonly onCompositionEnd: Event<void> = this._onCompositionEnd.event;
150
151
private _onSelectionChangeRequest = this._register(new Emitter<Selection>());
152
public readonly onSelectionChangeRequest: Event<Selection> = this._onSelectionChangeRequest.event;
153
154
// ---
155
156
private readonly _asyncTriggerCut: RunOnceScheduler;
157
158
private readonly _asyncFocusGainWriteScreenReaderContent: MutableDisposable<RunOnceScheduler> = this._register(new MutableDisposable());
159
160
private _textAreaState: TextAreaState;
161
162
public get textAreaState(): TextAreaState {
163
return this._textAreaState;
164
}
165
166
private _selectionChangeListener: IDisposable | null;
167
168
private _hasFocus: boolean;
169
private _currentComposition: CompositionContext | null;
170
171
constructor(
172
private readonly _host: ITextAreaInputHost,
173
private readonly _textArea: ICompleteTextAreaWrapper,
174
private readonly _OS: OperatingSystem,
175
private readonly _browser: IBrowser,
176
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
177
@ILogService private readonly _logService: ILogService
178
) {
179
super();
180
this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));
181
this._textAreaState = TextAreaState.EMPTY;
182
this._selectionChangeListener = null;
183
if (this._accessibilityService.isScreenReaderOptimized()) {
184
this.writeNativeTextAreaContent('ctor');
185
}
186
this._register(Event.runAndSubscribe(this._accessibilityService.onDidChangeScreenReaderOptimized, () => {
187
if (this._accessibilityService.isScreenReaderOptimized() && !this._asyncFocusGainWriteScreenReaderContent.value) {
188
// Don't use this._register() here - the MutableDisposable already handles cleanup
189
this._asyncFocusGainWriteScreenReaderContent.value = new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0);
190
} else {
191
this._asyncFocusGainWriteScreenReaderContent.clear();
192
}
193
}));
194
this._hasFocus = false;
195
this._currentComposition = null;
196
197
let lastKeyDown: IKeyboardEvent | null = null;
198
199
this._register(this._textArea.onKeyDown((_e) => {
200
const e = new StandardKeyboardEvent(_e);
201
if (e.keyCode === KeyCode.KEY_IN_COMPOSITION
202
|| (this._currentComposition && e.keyCode === KeyCode.Backspace)) {
203
// Stop propagation for keyDown events if the IME is processing key input
204
e.stopPropagation();
205
}
206
207
if (e.equals(KeyCode.Escape)) {
208
// Prevent default always for `Esc`, otherwise it will generate a keypress
209
// See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx
210
e.preventDefault();
211
}
212
213
lastKeyDown = e;
214
this._onKeyDown.fire(e);
215
}));
216
217
this._register(this._textArea.onKeyUp((_e) => {
218
const e = new StandardKeyboardEvent(_e);
219
this._onKeyUp.fire(e);
220
}));
221
222
this._register(this._textArea.onCompositionStart((e) => {
223
if (_debugComposition) {
224
console.log(`[compositionstart]`, e);
225
}
226
227
const currentComposition = new CompositionContext();
228
if (this._currentComposition) {
229
// simply reset the composition context
230
this._currentComposition = currentComposition;
231
return;
232
}
233
this._currentComposition = currentComposition;
234
235
if (
236
this._OS === OperatingSystem.Macintosh
237
&& lastKeyDown
238
&& lastKeyDown.equals(KeyCode.KEY_IN_COMPOSITION)
239
&& this._textAreaState.selectionStart === this._textAreaState.selectionEnd
240
&& this._textAreaState.selectionStart > 0
241
&& this._textAreaState.value.substr(this._textAreaState.selectionStart - 1, 1) === e.data
242
&& (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft')
243
) {
244
// Handling long press case on Chromium/Safari macOS + arrow key => pretend the character was selected
245
if (_debugComposition) {
246
console.log(`[compositionstart] Handling long press case on macOS + arrow key`, e);
247
}
248
// Pretend the previous character was composed (in order to get it removed by subsequent compositionupdate events)
249
currentComposition.handleCompositionUpdate('x');
250
this._onCompositionStart.fire({ data: e.data });
251
return;
252
}
253
254
if (this._browser.isAndroid) {
255
// when tapping on the editor, Android enters composition mode to edit the current word
256
// so we cannot clear the textarea on Android and we must pretend the current word was selected
257
this._onCompositionStart.fire({ data: e.data });
258
return;
259
}
260
261
this._onCompositionStart.fire({ data: e.data });
262
}));
263
264
this._register(this._textArea.onCompositionUpdate((e) => {
265
if (_debugComposition) {
266
console.log(`[compositionupdate]`, e);
267
}
268
const currentComposition = this._currentComposition;
269
if (!currentComposition) {
270
// should not be possible to receive a 'compositionupdate' without a 'compositionstart'
271
return;
272
}
273
if (this._browser.isAndroid) {
274
// On Android, the data sent with the composition update event is unusable.
275
// For example, if the cursor is in the middle of a word like Mic|osoft
276
// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".
277
// This is not really usable because it doesn't tell us where the edit began and where it ended.
278
const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
279
const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);
280
this._textAreaState = newState;
281
this._onType.fire(typeInput);
282
this._onCompositionUpdate.fire(e);
283
return;
284
}
285
const typeInput = currentComposition.handleCompositionUpdate(e.data);
286
this._textAreaState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
287
this._onType.fire(typeInput);
288
this._onCompositionUpdate.fire(e);
289
}));
290
291
this._register(this._textArea.onCompositionEnd((e) => {
292
if (_debugComposition) {
293
console.log(`[compositionend]`, e);
294
}
295
const currentComposition = this._currentComposition;
296
if (!currentComposition) {
297
// https://github.com/microsoft/monaco-editor/issues/1663
298
// On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data
299
return;
300
}
301
this._currentComposition = null;
302
303
if (this._browser.isAndroid) {
304
// On Android, the data sent with the composition update event is unusable.
305
// For example, if the cursor is in the middle of a word like Mic|osoft
306
// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".
307
// This is not really usable because it doesn't tell us where the edit began and where it ended.
308
const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
309
const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);
310
this._textAreaState = newState;
311
this._onType.fire(typeInput);
312
this._onCompositionEnd.fire();
313
return;
314
}
315
316
const typeInput = currentComposition.handleCompositionUpdate(e.data);
317
this._textAreaState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
318
this._onType.fire(typeInput);
319
this._onCompositionEnd.fire();
320
}));
321
322
this._register(this._textArea.onInput((e) => {
323
if (_debugComposition) {
324
console.log(`[input]`, e);
325
}
326
327
// Pretend here we touched the text area, as the `input` event will most likely
328
// result in a `selectionchange` event which we want to ignore
329
this._textArea.setIgnoreSelectionChangeTime('received input event');
330
331
if (this._currentComposition) {
332
return;
333
}
334
335
const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
336
const typeInput = TextAreaState.deduceInput(this._textAreaState, newState, /*couldBeEmojiInput*/this._OS === OperatingSystem.Macintosh);
337
338
if (typeInput.replacePrevCharCnt === 0 && typeInput.text.length === 1) {
339
// one character was typed
340
if (
341
strings.isHighSurrogate(typeInput.text.charCodeAt(0))
342
|| typeInput.text.charCodeAt(0) === 0x7f /* Delete */
343
) {
344
// Ignore invalid input but keep it around for next time
345
return;
346
}
347
}
348
349
this._textAreaState = newState;
350
if (
351
typeInput.text !== ''
352
|| typeInput.replacePrevCharCnt !== 0
353
|| typeInput.replaceNextCharCnt !== 0
354
|| typeInput.positionDelta !== 0
355
) {
356
// https://w3c.github.io/input-events/#interface-InputEvent-Attributes
357
if (e.inputType === 'insertFromPaste') {
358
this._onPaste.fire({
359
text: typeInput.text,
360
metadata: InMemoryClipboardMetadataManager.INSTANCE.get(typeInput.text)
361
});
362
} else {
363
this._onType.fire(typeInput);
364
}
365
}
366
}));
367
368
// --- Clipboard operations
369
370
this._register(this._textArea.onCut((e) => {
371
this._logService.trace(`TextAreaInput#onCut`, e);
372
373
// Fire onWillCut event to allow interception
374
const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._host.context, this._logService, this._browser.isFirefox);
375
this._onWillCut.fire(cutEvent);
376
if (cutEvent.isHandled) {
377
// Event was handled externally, skip default processing
378
return;
379
}
380
381
// Pretend here we touched the text area, as the `cut` event will most likely
382
// result in a `selectionchange` event which we want to ignore
383
this._textArea.setIgnoreSelectionChangeTime('received cut event');
384
385
cutEvent.ensureClipboardGetsEditorData();
386
this._asyncTriggerCut.schedule();
387
}));
388
389
this._register(this._textArea.onCopy((e) => {
390
this._logService.trace(`TextAreaInput#onCopy`, e);
391
392
// !!!!!
393
// This is a workaround for what we think is an Electron bug where
394
// execCommand('copy') does not always work (it does not fire a clipboard event)
395
// !!!!!
396
// We signal that we have executed a copy command
397
CopyOptions.electronBugWorkaroundCopyEventHasFired = true;
398
399
// Fire onWillCopy event to allow interception
400
const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._host.context, this._logService, this._browser.isFirefox);
401
this._onWillCopy.fire(copyEvent);
402
if (copyEvent.isHandled) {
403
// Event was handled externally, skip default processing
404
return;
405
}
406
407
copyEvent.ensureClipboardGetsEditorData();
408
}));
409
410
this._register(this._textArea.onPaste((e) => {
411
this._logService.trace(`TextAreaInput#onPaste`, e);
412
413
// Fire onWillPaste event to allow interception
414
const pasteEvent = createClipboardPasteEvent(e);
415
this._onWillPaste.fire(pasteEvent);
416
if (pasteEvent.isHandled) {
417
// Event was handled externally, skip default processing
418
return;
419
}
420
421
// Pretend here we touched the text area, as the `paste` event will most likely
422
// result in a `selectionchange` event which we want to ignore
423
this._textArea.setIgnoreSelectionChangeTime('received paste event');
424
425
e.preventDefault();
426
427
this._logService.trace(`TextAreaInput#onPaste with id : `, pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length);
428
if (!pasteEvent.text) {
429
return;
430
}
431
432
this._logService.trace(`TextAreaInput#onPaste (before onPaste)`);
433
this._onPaste.fire({
434
text: pasteEvent.text,
435
metadata: pasteEvent.metadata
436
});
437
}));
438
439
this._register(this._textArea.onFocus(() => {
440
const hadFocus = this._hasFocus;
441
442
this._setHasFocus(true);
443
444
if (this._accessibilityService.isScreenReaderOptimized() && this._browser.isSafari && !hadFocus && this._hasFocus) {
445
// When "tabbing into" the textarea, immediately after dispatching the 'focus' event,
446
// Safari will always move the selection at offset 0 in the textarea
447
if (!this._asyncFocusGainWriteScreenReaderContent.value) {
448
this._asyncFocusGainWriteScreenReaderContent.value = new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0);
449
}
450
this._asyncFocusGainWriteScreenReaderContent.value.schedule();
451
}
452
}));
453
this._register(this._textArea.onBlur(() => {
454
if (this._currentComposition) {
455
// See https://github.com/microsoft/vscode/issues/112621
456
// where compositionend is not triggered when the editor
457
// is taken off-dom during a composition
458
459
// Clear the flag to be able to write to the textarea
460
this._currentComposition = null;
461
462
// Clear the textarea to avoid an unwanted cursor type
463
this.writeNativeTextAreaContent('blurWithoutCompositionEnd');
464
465
// Fire artificial composition end
466
this._onCompositionEnd.fire();
467
}
468
this._setHasFocus(false);
469
}));
470
this._register(this._textArea.onSyntheticTap(() => {
471
if (this._browser.isAndroid && this._currentComposition) {
472
// on Android, tapping does not cancel the current composition, so the
473
// textarea is stuck showing the old composition
474
475
// Clear the flag to be able to write to the textarea
476
this._currentComposition = null;
477
478
// Clear the textarea to avoid an unwanted cursor type
479
this.writeNativeTextAreaContent('tapWithoutCompositionEnd');
480
481
// Fire artificial composition end
482
this._onCompositionEnd.fire();
483
}
484
}));
485
}
486
487
_initializeFromTest(): void {
488
this._hasFocus = true;
489
this._textAreaState = TextAreaState.readFromTextArea(this._textArea, null);
490
}
491
492
private _installSelectionChangeListener(): IDisposable {
493
// See https://github.com/microsoft/vscode/issues/27216 and https://github.com/microsoft/vscode/issues/98256
494
// When using a Braille display, it is possible for users to reposition the
495
// system caret. This is reflected in Chrome as a `selectionchange` event.
496
//
497
// The `selectionchange` event appears to be emitted under numerous other circumstances,
498
// so it is quite a challenge to distinguish a `selectionchange` coming in from a user
499
// using a Braille display from all the other cases.
500
//
501
// The problems with the `selectionchange` event are:
502
// * the event is emitted when the textarea is focused programmatically -- textarea.focus()
503
// * the event is emitted when the selection is changed in the textarea programmatically -- textarea.setSelectionRange(...)
504
// * the event is emitted when the value of the textarea is changed programmatically -- textarea.value = '...'
505
// * the event is emitted when tabbing into the textarea
506
// * the event is emitted asynchronously (sometimes with a delay as high as a few tens of ms)
507
// * the event sometimes comes in bursts for a single logical textarea operation
508
509
// `selectionchange` events often come multiple times for a single logical change
510
// so throttle multiple `selectionchange` events that burst in a short period of time.
511
let previousSelectionChangeEventTime = 0;
512
return dom.addDisposableListener(this._textArea.ownerDocument, 'selectionchange', (e) => {//todo
513
inputLatency.onSelectionChange();
514
515
if (!this._hasFocus) {
516
return;
517
}
518
if (this._currentComposition) {
519
return;
520
}
521
if (!this._browser.isChrome) {
522
// Support only for Chrome until testing happens on other browsers
523
return;
524
}
525
526
const now = Date.now();
527
528
const delta1 = now - previousSelectionChangeEventTime;
529
previousSelectionChangeEventTime = now;
530
if (delta1 < 5) {
531
// received another `selectionchange` event within 5ms of the previous `selectionchange` event
532
// => ignore it
533
return;
534
}
535
536
const delta2 = now - this._textArea.getIgnoreSelectionChangeTime();
537
this._textArea.resetSelectionChangeTime();
538
if (delta2 < 100) {
539
// received a `selectionchange` event within 100ms since we touched the textarea
540
// => ignore it, since we caused it
541
return;
542
}
543
544
if (!this._textAreaState.selection) {
545
// Cannot correlate a position in the textarea with a position in the editor...
546
return;
547
}
548
549
const newValue = this._textArea.getValue();
550
if (this._textAreaState.value !== newValue) {
551
// Cannot correlate a position in the textarea with a position in the editor...
552
return;
553
}
554
555
const newSelectionStart = this._textArea.getSelectionStart();
556
const newSelectionEnd = this._textArea.getSelectionEnd();
557
if (this._textAreaState.selectionStart === newSelectionStart && this._textAreaState.selectionEnd === newSelectionEnd) {
558
// Nothing to do...
559
return;
560
}
561
562
const _newSelectionStartPosition = this._textAreaState.deduceEditorPosition(newSelectionStart);
563
const newSelectionStartPosition = this._host.deduceModelPosition(_newSelectionStartPosition[0]!, _newSelectionStartPosition[1], _newSelectionStartPosition[2]);
564
565
const _newSelectionEndPosition = this._textAreaState.deduceEditorPosition(newSelectionEnd);
566
const newSelectionEndPosition = this._host.deduceModelPosition(_newSelectionEndPosition[0]!, _newSelectionEndPosition[1], _newSelectionEndPosition[2]);
567
568
const newSelection = new Selection(
569
newSelectionStartPosition.lineNumber, newSelectionStartPosition.column,
570
newSelectionEndPosition.lineNumber, newSelectionEndPosition.column
571
);
572
573
this._onSelectionChangeRequest.fire(newSelection);
574
});
575
}
576
577
public override dispose(): void {
578
super.dispose();
579
if (this._selectionChangeListener) {
580
this._selectionChangeListener.dispose();
581
this._selectionChangeListener = null;
582
}
583
}
584
585
public focusTextArea(): void {
586
// Setting this._hasFocus and writing the screen reader content
587
// will result in a focus() and setSelectionRange() in the textarea
588
this._setHasFocus(true);
589
590
// If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus
591
this.refreshFocusState();
592
}
593
594
public isFocused(): boolean {
595
return this._hasFocus;
596
}
597
598
public refreshFocusState(): void {
599
this._setHasFocus(this._textArea.hasFocus());
600
}
601
602
private _setHasFocus(newHasFocus: boolean): void {
603
if (this._hasFocus === newHasFocus) {
604
// no change
605
return;
606
}
607
this._hasFocus = newHasFocus;
608
609
if (this._selectionChangeListener) {
610
this._selectionChangeListener.dispose();
611
this._selectionChangeListener = null;
612
}
613
if (this._hasFocus) {
614
this._selectionChangeListener = this._installSelectionChangeListener();
615
}
616
617
if (this._hasFocus) {
618
this.writeNativeTextAreaContent('focusgain');
619
}
620
621
if (this._hasFocus) {
622
this._onFocus.fire();
623
} else {
624
this._onBlur.fire();
625
}
626
}
627
628
private _setAndWriteTextAreaState(reason: string, textAreaState: TextAreaState): void {
629
if (!this._hasFocus) {
630
textAreaState = textAreaState.collapseSelection();
631
}
632
if (!textAreaState.isWrittenToTextArea(this._textArea, this._hasFocus)) {
633
this._logService.trace(`writeTextAreaState(reason: ${reason})`);
634
}
635
textAreaState.writeToTextArea(reason, this._textArea, this._hasFocus);
636
this._textAreaState = textAreaState;
637
}
638
639
public writeNativeTextAreaContent(reason: string): void {
640
if ((!this._accessibilityService.isScreenReaderOptimized() && reason === 'render') || this._currentComposition) {
641
// Do not write to the text on render unless a screen reader is being used #192278
642
// Do not write to the text area when doing composition
643
return;
644
}
645
this._setAndWriteTextAreaState(reason, this._host.getScreenReaderContent());
646
}
647
}
648
649
export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrapper {
650
651
public readonly onKeyDown: Event<KeyboardEvent>;
652
public readonly onKeyPress: Event<KeyboardEvent>;
653
public readonly onKeyUp: Event<KeyboardEvent>;
654
public readonly onCompositionStart: Event<CompositionEvent>;
655
public readonly onCompositionUpdate: Event<CompositionEvent>;
656
public readonly onCompositionEnd: Event<CompositionEvent>;
657
public readonly onBeforeInput: Event<InputEvent>;
658
public readonly onInput: Event<InputEvent>;
659
public readonly onCut: Event<ClipboardEvent>;
660
public readonly onCopy: Event<ClipboardEvent>;
661
public readonly onPaste: Event<ClipboardEvent>;
662
public readonly onFocus: Event<FocusEvent>;
663
public readonly onBlur: Event<FocusEvent>; // = this._register(new DomEmitter(this._actual, 'blur')).event;
664
665
public get ownerDocument(): Document {
666
return this._actual.ownerDocument;
667
}
668
669
private _onSyntheticTap = this._register(new Emitter<void>());
670
public readonly onSyntheticTap: Event<void> = this._onSyntheticTap.event;
671
672
private _ignoreSelectionChangeTime: number;
673
674
constructor(
675
private readonly _actual: HTMLTextAreaElement
676
) {
677
super();
678
this._ignoreSelectionChangeTime = 0;
679
this.onKeyDown = this._register(new DomEmitter(this._actual, 'keydown')).event;
680
this.onKeyPress = this._register(new DomEmitter(this._actual, 'keypress')).event;
681
this.onKeyUp = this._register(new DomEmitter(this._actual, 'keyup')).event;
682
this.onCompositionStart = this._register(new DomEmitter(this._actual, 'compositionstart')).event;
683
this.onCompositionUpdate = this._register(new DomEmitter(this._actual, 'compositionupdate')).event;
684
this.onCompositionEnd = this._register(new DomEmitter(this._actual, 'compositionend')).event;
685
this.onBeforeInput = this._register(new DomEmitter(this._actual, 'beforeinput')).event;
686
this.onInput = <Event<InputEvent>>this._register(new DomEmitter(this._actual, 'input')).event;
687
this.onCut = this._register(new DomEmitter(this._actual, 'cut')).event;
688
this.onCopy = this._register(new DomEmitter(this._actual, 'copy')).event;
689
this.onPaste = this._register(new DomEmitter(this._actual, 'paste')).event;
690
this.onFocus = this._register(new DomEmitter(this._actual, 'focus')).event;
691
this.onBlur = this._register(new DomEmitter(this._actual, 'blur')).event;
692
693
this._register(this.onKeyDown(() => inputLatency.onKeyDown()));
694
this._register(this.onBeforeInput(() => inputLatency.onBeforeInput()));
695
this._register(this.onInput(() => inputLatency.onInput()));
696
this._register(this.onKeyUp(() => inputLatency.onKeyUp()));
697
this._register(dom.addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire()));
698
}
699
700
public hasFocus(): boolean {
701
const shadowRoot = dom.getShadowRoot(this._actual);
702
if (shadowRoot) {
703
return shadowRoot.activeElement === this._actual;
704
} else if (this._actual.isConnected) {
705
return dom.getActiveElement() === this._actual;
706
} else {
707
return false;
708
}
709
}
710
711
public setIgnoreSelectionChangeTime(reason: string): void {
712
this._ignoreSelectionChangeTime = Date.now();
713
}
714
715
public getIgnoreSelectionChangeTime(): number {
716
return this._ignoreSelectionChangeTime;
717
}
718
719
public resetSelectionChangeTime(): void {
720
this._ignoreSelectionChangeTime = 0;
721
}
722
723
public getValue(): string {
724
// console.log('current value: ' + this._textArea.value);
725
return this._actual.value;
726
}
727
728
public setValue(reason: string, value: string): void {
729
const textArea = this._actual;
730
if (textArea.value === value) {
731
// No change
732
return;
733
}
734
// console.log('reason: ' + reason + ', current value: ' + textArea.value + ' => new value: ' + value);
735
this.setIgnoreSelectionChangeTime('setValue');
736
textArea.value = value;
737
}
738
739
public getSelectionStart(): number {
740
return this._actual.selectionDirection === 'backward' ? this._actual.selectionEnd : this._actual.selectionStart;
741
}
742
743
public getSelectionEnd(): number {
744
return this._actual.selectionDirection === 'backward' ? this._actual.selectionStart : this._actual.selectionEnd;
745
}
746
747
public setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void {
748
const textArea = this._actual;
749
750
let activeElement: Element | null = null;
751
const shadowRoot = dom.getShadowRoot(textArea);
752
if (shadowRoot) {
753
activeElement = shadowRoot.activeElement;
754
} else {
755
activeElement = dom.getActiveElement();
756
}
757
const activeWindow = dom.getWindow(activeElement);
758
759
const currentIsFocused = (activeElement === textArea);
760
const currentSelectionStart = textArea.selectionStart;
761
const currentSelectionEnd = textArea.selectionEnd;
762
763
if (currentIsFocused && currentSelectionStart === selectionStart && currentSelectionEnd === selectionEnd) {
764
// No change
765
// Firefox iframe bug https://github.com/microsoft/monaco-editor/issues/643#issuecomment-367871377
766
if (browser.isFirefox && activeWindow.parent !== activeWindow) {
767
textArea.focus();
768
}
769
return;
770
}
771
772
// console.log('reason: ' + reason + ', setSelectionRange: ' + selectionStart + ' -> ' + selectionEnd);
773
774
if (currentIsFocused) {
775
// No need to focus, only need to change the selection range
776
this.setIgnoreSelectionChangeTime('setSelectionRange');
777
textArea.setSelectionRange(selectionStart, selectionEnd);
778
if (browser.isFirefox && activeWindow.parent !== activeWindow) {
779
textArea.focus();
780
}
781
return;
782
}
783
784
// If the focus is outside the textarea, browsers will try really hard to reveal the textarea.
785
// Here, we try to undo the browser's desperate reveal.
786
try {
787
const scrollState = dom.saveParentsScrollTop(textArea);
788
this.setIgnoreSelectionChangeTime('setSelectionRange');
789
textArea.focus();
790
textArea.setSelectionRange(selectionStart, selectionEnd);
791
dom.restoreParentsScrollTop(textArea, scrollState);
792
} catch (e) {
793
// Sometimes IE throws when setting selection (e.g. textarea is off-DOM)
794
}
795
}
796
}
797
798