Path: blob/main/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as browser from '../../../../../base/browser/browser.js';6import * as dom from '../../../../../base/browser/dom.js';7import { DomEmitter } from '../../../../../base/browser/event.js';8import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';9import { inputLatency } from '../../../../../base/browser/performance.js';10import { RunOnceScheduler } from '../../../../../base/common/async.js';11import { Emitter, Event } from '../../../../../base/common/event.js';12import { KeyCode } from '../../../../../base/common/keyCodes.js';13import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';14import { OperatingSystem } from '../../../../../base/common/platform.js';15import * as strings from '../../../../../base/common/strings.js';16import { Position } from '../../../../common/core/position.js';17import { Selection } from '../../../../common/core/selection.js';18import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';19import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js';20import { ClipboardDataToCopy, ClipboardEventUtils, ClipboardStoredMetadata, InMemoryClipboardMetadataManager } from '../clipboardUtils.js';21import { _debugComposition, ITextAreaWrapper, ITypeData, TextAreaState } from './textAreaEditContextState.js';22import { generateUuid } from '../../../../../base/common/uuid.js';2324export namespace TextAreaSyntethicEvents {25export const Tap = '-monaco-textarea-synthetic-tap';26}2728export interface ICompositionData {29data: string;30}313233export interface IPasteData {34text: string;35metadata: ClipboardStoredMetadata | null;36}3738export interface ITextAreaInputHost {39getDataToCopy(): ClipboardDataToCopy;40getScreenReaderContent(): TextAreaState;41deduceModelPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position;42}4344export interface ICompositionStartEvent {45data: string;46}4748export interface ICompleteTextAreaWrapper extends ITextAreaWrapper {49readonly onKeyDown: Event<KeyboardEvent>;50readonly onKeyPress: Event<KeyboardEvent>;51readonly onKeyUp: Event<KeyboardEvent>;52readonly onCompositionStart: Event<CompositionEvent>;53readonly onCompositionUpdate: Event<CompositionEvent>;54readonly onCompositionEnd: Event<CompositionEvent>;55readonly onBeforeInput: Event<InputEvent>;56readonly onInput: Event<InputEvent>;57readonly onCut: Event<ClipboardEvent>;58readonly onCopy: Event<ClipboardEvent>;59readonly onPaste: Event<ClipboardEvent>;60readonly onFocus: Event<FocusEvent>;61readonly onBlur: Event<FocusEvent>;62readonly onSyntheticTap: Event<void>;6364readonly ownerDocument: Document;6566setIgnoreSelectionChangeTime(reason: string): void;67getIgnoreSelectionChangeTime(): number;68resetSelectionChangeTime(): void;6970hasFocus(): boolean;71}7273export interface IBrowser {74isAndroid: boolean;75isFirefox: boolean;76isChrome: boolean;77isSafari: boolean;78}7980class CompositionContext {8182private _lastTypeTextLength: number;8384constructor() {85this._lastTypeTextLength = 0;86}8788public handleCompositionUpdate(text: string | null | undefined): ITypeData {89text = text || '';90const typeInput: ITypeData = {91text: text,92replacePrevCharCnt: this._lastTypeTextLength,93replaceNextCharCnt: 0,94positionDelta: 095};96this._lastTypeTextLength = text.length;97return typeInput;98}99}100101/**102* Writes screen reader content to the textarea and is able to analyze its input events to generate:103* - onCut104* - onPaste105* - onType106*107* Composition events are generated for presentation purposes (composition input is reflected in onType).108*/109export class TextAreaInput extends Disposable {110111private _onFocus = this._register(new Emitter<void>());112public readonly onFocus: Event<void> = this._onFocus.event;113114private _onBlur = this._register(new Emitter<void>());115public readonly onBlur: Event<void> = this._onBlur.event;116117private _onKeyDown = this._register(new Emitter<IKeyboardEvent>());118public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;119120private _onKeyUp = this._register(new Emitter<IKeyboardEvent>());121public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;122123private _onCut = this._register(new Emitter<void>());124public readonly onCut: Event<void> = this._onCut.event;125126private _onPaste = this._register(new Emitter<IPasteData>());127public readonly onPaste: Event<IPasteData> = this._onPaste.event;128129private _onType = this._register(new Emitter<ITypeData>());130public readonly onType: Event<ITypeData> = this._onType.event;131132private _onCompositionStart = this._register(new Emitter<ICompositionStartEvent>());133public readonly onCompositionStart: Event<ICompositionStartEvent> = this._onCompositionStart.event;134135private _onCompositionUpdate = this._register(new Emitter<ICompositionData>());136public readonly onCompositionUpdate: Event<ICompositionData> = this._onCompositionUpdate.event;137138private _onCompositionEnd = this._register(new Emitter<void>());139public readonly onCompositionEnd: Event<void> = this._onCompositionEnd.event;140141private _onSelectionChangeRequest = this._register(new Emitter<Selection>());142public readonly onSelectionChangeRequest: Event<Selection> = this._onSelectionChangeRequest.event;143144// ---145146private readonly _asyncTriggerCut: RunOnceScheduler;147148private readonly _asyncFocusGainWriteScreenReaderContent: MutableDisposable<RunOnceScheduler> = this._register(new MutableDisposable());149150private _textAreaState: TextAreaState;151152public get textAreaState(): TextAreaState {153return this._textAreaState;154}155156private _selectionChangeListener: IDisposable | null;157158private _hasFocus: boolean;159private _currentComposition: CompositionContext | null;160161constructor(162private readonly _host: ITextAreaInputHost,163private readonly _textArea: ICompleteTextAreaWrapper,164private readonly _OS: OperatingSystem,165private readonly _browser: IBrowser,166@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,167@ILogService private readonly _logService: ILogService168) {169super();170this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));171this._textAreaState = TextAreaState.EMPTY;172this._selectionChangeListener = null;173if (this._accessibilityService.isScreenReaderOptimized()) {174this.writeNativeTextAreaContent('ctor');175}176this._register(Event.runAndSubscribe(this._accessibilityService.onDidChangeScreenReaderOptimized, () => {177if (this._accessibilityService.isScreenReaderOptimized() && !this._asyncFocusGainWriteScreenReaderContent.value) {178this._asyncFocusGainWriteScreenReaderContent.value = this._register(new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0));179} else {180this._asyncFocusGainWriteScreenReaderContent.clear();181}182}));183this._hasFocus = false;184this._currentComposition = null;185186let lastKeyDown: IKeyboardEvent | null = null;187188this._register(this._textArea.onKeyDown((_e) => {189const e = new StandardKeyboardEvent(_e);190if (e.keyCode === KeyCode.KEY_IN_COMPOSITION191|| (this._currentComposition && e.keyCode === KeyCode.Backspace)) {192// Stop propagation for keyDown events if the IME is processing key input193e.stopPropagation();194}195196if (e.equals(KeyCode.Escape)) {197// Prevent default always for `Esc`, otherwise it will generate a keypress198// See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx199e.preventDefault();200}201202lastKeyDown = e;203this._onKeyDown.fire(e);204}));205206this._register(this._textArea.onKeyUp((_e) => {207const e = new StandardKeyboardEvent(_e);208this._onKeyUp.fire(e);209}));210211this._register(this._textArea.onCompositionStart((e) => {212if (_debugComposition) {213console.log(`[compositionstart]`, e);214}215216const currentComposition = new CompositionContext();217if (this._currentComposition) {218// simply reset the composition context219this._currentComposition = currentComposition;220return;221}222this._currentComposition = currentComposition;223224if (225this._OS === OperatingSystem.Macintosh226&& lastKeyDown227&& lastKeyDown.equals(KeyCode.KEY_IN_COMPOSITION)228&& this._textAreaState.selectionStart === this._textAreaState.selectionEnd229&& this._textAreaState.selectionStart > 0230&& this._textAreaState.value.substr(this._textAreaState.selectionStart - 1, 1) === e.data231&& (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft')232) {233// Handling long press case on Chromium/Safari macOS + arrow key => pretend the character was selected234if (_debugComposition) {235console.log(`[compositionstart] Handling long press case on macOS + arrow key`, e);236}237// Pretend the previous character was composed (in order to get it removed by subsequent compositionupdate events)238currentComposition.handleCompositionUpdate('x');239this._onCompositionStart.fire({ data: e.data });240return;241}242243if (this._browser.isAndroid) {244// when tapping on the editor, Android enters composition mode to edit the current word245// so we cannot clear the textarea on Android and we must pretend the current word was selected246this._onCompositionStart.fire({ data: e.data });247return;248}249250this._onCompositionStart.fire({ data: e.data });251}));252253this._register(this._textArea.onCompositionUpdate((e) => {254if (_debugComposition) {255console.log(`[compositionupdate]`, e);256}257const currentComposition = this._currentComposition;258if (!currentComposition) {259// should not be possible to receive a 'compositionupdate' without a 'compositionstart'260return;261}262if (this._browser.isAndroid) {263// On Android, the data sent with the composition update event is unusable.264// For example, if the cursor is in the middle of a word like Mic|osoft265// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".266// This is not really usable because it doesn't tell us where the edit began and where it ended.267const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);268const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);269this._textAreaState = newState;270this._onType.fire(typeInput);271this._onCompositionUpdate.fire(e);272return;273}274const typeInput = currentComposition.handleCompositionUpdate(e.data);275this._textAreaState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);276this._onType.fire(typeInput);277this._onCompositionUpdate.fire(e);278}));279280this._register(this._textArea.onCompositionEnd((e) => {281if (_debugComposition) {282console.log(`[compositionend]`, e);283}284const currentComposition = this._currentComposition;285if (!currentComposition) {286// https://github.com/microsoft/monaco-editor/issues/1663287// On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data288return;289}290this._currentComposition = null;291292if (this._browser.isAndroid) {293// On Android, the data sent with the composition update event is unusable.294// For example, if the cursor is in the middle of a word like Mic|osoft295// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".296// This is not really usable because it doesn't tell us where the edit began and where it ended.297const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);298const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);299this._textAreaState = newState;300this._onType.fire(typeInput);301this._onCompositionEnd.fire();302return;303}304305const typeInput = currentComposition.handleCompositionUpdate(e.data);306this._textAreaState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);307this._onType.fire(typeInput);308this._onCompositionEnd.fire();309}));310311this._register(this._textArea.onInput((e) => {312if (_debugComposition) {313console.log(`[input]`, e);314}315316// Pretend here we touched the text area, as the `input` event will most likely317// result in a `selectionchange` event which we want to ignore318this._textArea.setIgnoreSelectionChangeTime('received input event');319320if (this._currentComposition) {321return;322}323324const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);325const typeInput = TextAreaState.deduceInput(this._textAreaState, newState, /*couldBeEmojiInput*/this._OS === OperatingSystem.Macintosh);326327if (typeInput.replacePrevCharCnt === 0 && typeInput.text.length === 1) {328// one character was typed329if (330strings.isHighSurrogate(typeInput.text.charCodeAt(0))331|| typeInput.text.charCodeAt(0) === 0x7f /* Delete */332) {333// Ignore invalid input but keep it around for next time334return;335}336}337338this._textAreaState = newState;339if (340typeInput.text !== ''341|| typeInput.replacePrevCharCnt !== 0342|| typeInput.replaceNextCharCnt !== 0343|| typeInput.positionDelta !== 0344) {345// https://w3c.github.io/input-events/#interface-InputEvent-Attributes346if (e.inputType === 'insertFromPaste') {347this._onPaste.fire({348text: typeInput.text,349metadata: InMemoryClipboardMetadataManager.INSTANCE.get(typeInput.text)350});351} else {352this._onType.fire(typeInput);353}354}355}));356357// --- Clipboard operations358359this._register(this._textArea.onCut((e) => {360this._logService.trace(`TextAreaInput#onCut`, e);361// Pretend here we touched the text area, as the `cut` event will most likely362// result in a `selectionchange` event which we want to ignore363this._textArea.setIgnoreSelectionChangeTime('received cut event');364365this._ensureClipboardGetsEditorSelection(e);366this._asyncTriggerCut.schedule();367}));368369this._register(this._textArea.onCopy((e) => {370this._logService.trace(`TextAreaInput#onCopy`, e);371this._ensureClipboardGetsEditorSelection(e);372}));373374this._register(this._textArea.onPaste((e) => {375this._logService.trace(`TextAreaInput#onPaste`, e);376// Pretend here we touched the text area, as the `paste` event will most likely377// result in a `selectionchange` event which we want to ignore378this._textArea.setIgnoreSelectionChangeTime('received paste event');379380e.preventDefault();381382if (!e.clipboardData) {383return;384}385386let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData);387this._logService.trace(`TextAreaInput#onPaste with id : `, metadata?.id, ' with text.length: ', text.length);388if (!text) {389return;390}391392// try the in-memory store393metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text);394395this._logService.trace(`TextAreaInput#onPaste (before onPaste)`);396this._onPaste.fire({397text: text,398metadata: metadata399});400}));401402this._register(this._textArea.onFocus(() => {403const hadFocus = this._hasFocus;404405this._setHasFocus(true);406407if (this._accessibilityService.isScreenReaderOptimized() && this._browser.isSafari && !hadFocus && this._hasFocus) {408// When "tabbing into" the textarea, immediately after dispatching the 'focus' event,409// Safari will always move the selection at offset 0 in the textarea410if (!this._asyncFocusGainWriteScreenReaderContent.value) {411this._asyncFocusGainWriteScreenReaderContent.value = new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0);412}413this._asyncFocusGainWriteScreenReaderContent.value.schedule();414}415}));416this._register(this._textArea.onBlur(() => {417if (this._currentComposition) {418// See https://github.com/microsoft/vscode/issues/112621419// where compositionend is not triggered when the editor420// is taken off-dom during a composition421422// Clear the flag to be able to write to the textarea423this._currentComposition = null;424425// Clear the textarea to avoid an unwanted cursor type426this.writeNativeTextAreaContent('blurWithoutCompositionEnd');427428// Fire artificial composition end429this._onCompositionEnd.fire();430}431this._setHasFocus(false);432}));433this._register(this._textArea.onSyntheticTap(() => {434if (this._browser.isAndroid && this._currentComposition) {435// on Android, tapping does not cancel the current composition, so the436// textarea is stuck showing the old composition437438// Clear the flag to be able to write to the textarea439this._currentComposition = null;440441// Clear the textarea to avoid an unwanted cursor type442this.writeNativeTextAreaContent('tapWithoutCompositionEnd');443444// Fire artificial composition end445this._onCompositionEnd.fire();446}447}));448}449450_initializeFromTest(): void {451this._hasFocus = true;452this._textAreaState = TextAreaState.readFromTextArea(this._textArea, null);453}454455private _installSelectionChangeListener(): IDisposable {456// See https://github.com/microsoft/vscode/issues/27216 and https://github.com/microsoft/vscode/issues/98256457// When using a Braille display, it is possible for users to reposition the458// system caret. This is reflected in Chrome as a `selectionchange` event.459//460// The `selectionchange` event appears to be emitted under numerous other circumstances,461// so it is quite a challenge to distinguish a `selectionchange` coming in from a user462// using a Braille display from all the other cases.463//464// The problems with the `selectionchange` event are:465// * the event is emitted when the textarea is focused programmatically -- textarea.focus()466// * the event is emitted when the selection is changed in the textarea programmatically -- textarea.setSelectionRange(...)467// * the event is emitted when the value of the textarea is changed programmatically -- textarea.value = '...'468// * the event is emitted when tabbing into the textarea469// * the event is emitted asynchronously (sometimes with a delay as high as a few tens of ms)470// * the event sometimes comes in bursts for a single logical textarea operation471472// `selectionchange` events often come multiple times for a single logical change473// so throttle multiple `selectionchange` events that burst in a short period of time.474let previousSelectionChangeEventTime = 0;475return dom.addDisposableListener(this._textArea.ownerDocument, 'selectionchange', (e) => {//todo476inputLatency.onSelectionChange();477478if (!this._hasFocus) {479return;480}481if (this._currentComposition) {482return;483}484if (!this._browser.isChrome) {485// Support only for Chrome until testing happens on other browsers486return;487}488489const now = Date.now();490491const delta1 = now - previousSelectionChangeEventTime;492previousSelectionChangeEventTime = now;493if (delta1 < 5) {494// received another `selectionchange` event within 5ms of the previous `selectionchange` event495// => ignore it496return;497}498499const delta2 = now - this._textArea.getIgnoreSelectionChangeTime();500this._textArea.resetSelectionChangeTime();501if (delta2 < 100) {502// received a `selectionchange` event within 100ms since we touched the textarea503// => ignore it, since we caused it504return;505}506507if (!this._textAreaState.selection) {508// Cannot correlate a position in the textarea with a position in the editor...509return;510}511512const newValue = this._textArea.getValue();513if (this._textAreaState.value !== newValue) {514// Cannot correlate a position in the textarea with a position in the editor...515return;516}517518const newSelectionStart = this._textArea.getSelectionStart();519const newSelectionEnd = this._textArea.getSelectionEnd();520if (this._textAreaState.selectionStart === newSelectionStart && this._textAreaState.selectionEnd === newSelectionEnd) {521// Nothing to do...522return;523}524525const _newSelectionStartPosition = this._textAreaState.deduceEditorPosition(newSelectionStart);526const newSelectionStartPosition = this._host.deduceModelPosition(_newSelectionStartPosition[0]!, _newSelectionStartPosition[1], _newSelectionStartPosition[2]);527528const _newSelectionEndPosition = this._textAreaState.deduceEditorPosition(newSelectionEnd);529const newSelectionEndPosition = this._host.deduceModelPosition(_newSelectionEndPosition[0]!, _newSelectionEndPosition[1], _newSelectionEndPosition[2]);530531const newSelection = new Selection(532newSelectionStartPosition.lineNumber, newSelectionStartPosition.column,533newSelectionEndPosition.lineNumber, newSelectionEndPosition.column534);535536this._onSelectionChangeRequest.fire(newSelection);537});538}539540public override dispose(): void {541super.dispose();542if (this._selectionChangeListener) {543this._selectionChangeListener.dispose();544this._selectionChangeListener = null;545}546}547548public focusTextArea(): void {549// Setting this._hasFocus and writing the screen reader content550// will result in a focus() and setSelectionRange() in the textarea551this._setHasFocus(true);552553// If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus554this.refreshFocusState();555}556557public isFocused(): boolean {558return this._hasFocus;559}560561public refreshFocusState(): void {562this._setHasFocus(this._textArea.hasFocus());563}564565private _setHasFocus(newHasFocus: boolean): void {566if (this._hasFocus === newHasFocus) {567// no change568return;569}570this._hasFocus = newHasFocus;571572if (this._selectionChangeListener) {573this._selectionChangeListener.dispose();574this._selectionChangeListener = null;575}576if (this._hasFocus) {577this._selectionChangeListener = this._installSelectionChangeListener();578}579580if (this._hasFocus) {581this.writeNativeTextAreaContent('focusgain');582}583584if (this._hasFocus) {585this._onFocus.fire();586} else {587this._onBlur.fire();588}589}590591private _setAndWriteTextAreaState(reason: string, textAreaState: TextAreaState): void {592if (!this._hasFocus) {593textAreaState = textAreaState.collapseSelection();594}595if (!textAreaState.isWrittenToTextArea(this._textArea, this._hasFocus)) {596this._logService.trace(`writeTextAreaState(reason: ${reason})`);597}598textAreaState.writeToTextArea(reason, this._textArea, this._hasFocus);599this._textAreaState = textAreaState;600}601602public writeNativeTextAreaContent(reason: string): void {603if ((!this._accessibilityService.isScreenReaderOptimized() && reason === 'render') || this._currentComposition) {604// Do not write to the text on render unless a screen reader is being used #192278605// Do not write to the text area when doing composition606return;607}608this._setAndWriteTextAreaState(reason, this._host.getScreenReaderContent());609}610611private _ensureClipboardGetsEditorSelection(e: ClipboardEvent): void {612const dataToCopy = this._host.getDataToCopy();613let id = undefined;614if (this._logService.getLevel() === LogLevel.Trace) {615id = generateUuid();616}617const storedMetadata: ClipboardStoredMetadata = {618version: 1,619id,620isFromEmptySelection: dataToCopy.isFromEmptySelection,621multicursorText: dataToCopy.multicursorText,622mode: dataToCopy.mode623};624InMemoryClipboardMetadataManager.INSTANCE.set(625// When writing "LINE\r\n" to the clipboard and then pasting,626// Firefox pastes "LINE\n", so let's work around this quirk627(this._browser.isFirefox ? dataToCopy.text.replace(/\r\n/g, '\n') : dataToCopy.text),628storedMetadata629);630631e.preventDefault();632if (e.clipboardData) {633ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, storedMetadata);634}635this._logService.trace('TextAreaEditContextInput#_ensureClipboardGetsEditorSelection with id : ', id, ' with text.length: ', dataToCopy.text.length);636}637}638639export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrapper {640641public readonly onKeyDown: Event<KeyboardEvent>;642public readonly onKeyPress: Event<KeyboardEvent>;643public readonly onKeyUp: Event<KeyboardEvent>;644public readonly onCompositionStart: Event<CompositionEvent>;645public readonly onCompositionUpdate: Event<CompositionEvent>;646public readonly onCompositionEnd: Event<CompositionEvent>;647public readonly onBeforeInput: Event<InputEvent>;648public readonly onInput: Event<InputEvent>;649public readonly onCut: Event<ClipboardEvent>;650public readonly onCopy: Event<ClipboardEvent>;651public readonly onPaste: Event<ClipboardEvent>;652public readonly onFocus: Event<FocusEvent>;653public readonly onBlur: Event<FocusEvent>; // = this._register(new DomEmitter(this._actual, 'blur')).event;654655public get ownerDocument(): Document {656return this._actual.ownerDocument;657}658659private _onSyntheticTap = this._register(new Emitter<void>());660public readonly onSyntheticTap: Event<void> = this._onSyntheticTap.event;661662private _ignoreSelectionChangeTime: number;663664constructor(665private readonly _actual: HTMLTextAreaElement666) {667super();668this._ignoreSelectionChangeTime = 0;669this.onKeyDown = this._register(new DomEmitter(this._actual, 'keydown')).event;670this.onKeyPress = this._register(new DomEmitter(this._actual, 'keypress')).event;671this.onKeyUp = this._register(new DomEmitter(this._actual, 'keyup')).event;672this.onCompositionStart = this._register(new DomEmitter(this._actual, 'compositionstart')).event;673this.onCompositionUpdate = this._register(new DomEmitter(this._actual, 'compositionupdate')).event;674this.onCompositionEnd = this._register(new DomEmitter(this._actual, 'compositionend')).event;675this.onBeforeInput = this._register(new DomEmitter(this._actual, 'beforeinput')).event;676this.onInput = <Event<InputEvent>>this._register(new DomEmitter(this._actual, 'input')).event;677this.onCut = this._register(new DomEmitter(this._actual, 'cut')).event;678this.onCopy = this._register(new DomEmitter(this._actual, 'copy')).event;679this.onPaste = this._register(new DomEmitter(this._actual, 'paste')).event;680this.onFocus = this._register(new DomEmitter(this._actual, 'focus')).event;681this.onBlur = this._register(new DomEmitter(this._actual, 'blur')).event;682683this._register(this.onKeyDown(() => inputLatency.onKeyDown()));684this._register(this.onBeforeInput(() => inputLatency.onBeforeInput()));685this._register(this.onInput(() => inputLatency.onInput()));686this._register(this.onKeyUp(() => inputLatency.onKeyUp()));687this._register(dom.addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire()));688}689690public hasFocus(): boolean {691const shadowRoot = dom.getShadowRoot(this._actual);692if (shadowRoot) {693return shadowRoot.activeElement === this._actual;694} else if (this._actual.isConnected) {695return dom.getActiveElement() === this._actual;696} else {697return false;698}699}700701public setIgnoreSelectionChangeTime(reason: string): void {702this._ignoreSelectionChangeTime = Date.now();703}704705public getIgnoreSelectionChangeTime(): number {706return this._ignoreSelectionChangeTime;707}708709public resetSelectionChangeTime(): void {710this._ignoreSelectionChangeTime = 0;711}712713public getValue(): string {714// console.log('current value: ' + this._textArea.value);715return this._actual.value;716}717718public setValue(reason: string, value: string): void {719const textArea = this._actual;720if (textArea.value === value) {721// No change722return;723}724// console.log('reason: ' + reason + ', current value: ' + textArea.value + ' => new value: ' + value);725this.setIgnoreSelectionChangeTime('setValue');726textArea.value = value;727}728729public getSelectionStart(): number {730return this._actual.selectionDirection === 'backward' ? this._actual.selectionEnd : this._actual.selectionStart;731}732733public getSelectionEnd(): number {734return this._actual.selectionDirection === 'backward' ? this._actual.selectionStart : this._actual.selectionEnd;735}736737public setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void {738const textArea = this._actual;739740let activeElement: Element | null = null;741const shadowRoot = dom.getShadowRoot(textArea);742if (shadowRoot) {743activeElement = shadowRoot.activeElement;744} else {745activeElement = dom.getActiveElement();746}747const activeWindow = dom.getWindow(activeElement);748749const currentIsFocused = (activeElement === textArea);750const currentSelectionStart = textArea.selectionStart;751const currentSelectionEnd = textArea.selectionEnd;752753if (currentIsFocused && currentSelectionStart === selectionStart && currentSelectionEnd === selectionEnd) {754// No change755// Firefox iframe bug https://github.com/microsoft/monaco-editor/issues/643#issuecomment-367871377756if (browser.isFirefox && activeWindow.parent !== activeWindow) {757textArea.focus();758}759return;760}761762// console.log('reason: ' + reason + ', setSelectionRange: ' + selectionStart + ' -> ' + selectionEnd);763764if (currentIsFocused) {765// No need to focus, only need to change the selection range766this.setIgnoreSelectionChangeTime('setSelectionRange');767textArea.setSelectionRange(selectionStart, selectionEnd);768if (browser.isFirefox && activeWindow.parent !== activeWindow) {769textArea.focus();770}771return;772}773774// If the focus is outside the textarea, browsers will try really hard to reveal the textarea.775// Here, we try to undo the browser's desperate reveal.776try {777const scrollState = dom.saveParentsScrollTop(textArea);778this.setIgnoreSelectionChangeTime('setSelectionRange');779textArea.focus();780textArea.setSelectionRange(selectionStart, selectionEnd);781dom.restoreParentsScrollTop(textArea, scrollState);782} catch (e) {783// Sometimes IE throws when setting selection (e.g. textarea is off-DOM)784}785}786}787788789