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