in src/vs/editor/browser/controller/textAreaInput.ts [114:334]
constructor(host: ITextAreaInputHost, textArea: FastDomNode<HTMLTextAreaElement>) {
super();
this._host = host;
this._textArea = this._register(new TextAreaWrapper(textArea));
this._lastTextAreaEvent = TextAreaInputEventType.none;
this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));
this._textAreaState = TextAreaState.EMPTY;
this.writeScreenReaderContent('ctor');
this._hasFocus = false;
this._isDoingComposition = false;
this._nextCommand = ReadFromTextArea.Type;
this._register(dom.addStandardDisposableListener(textArea.domNode, 'keydown', (e: IKeyboardEvent) => {
if (this._isDoingComposition &&
(e.keyCode === KeyCode.KEY_IN_COMPOSITION || e.keyCode === KeyCode.Backspace)) {
// Stop propagation for keyDown events if the IME is processing key input
e.stopPropagation();
}
if (e.equals(KeyCode.Escape)) {
// Prevent default always for `Esc`, otherwise it will generate a keypress
// See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx
e.preventDefault();
}
this._onKeyDown.fire(e);
}));
this._register(dom.addStandardDisposableListener(textArea.domNode, 'keyup', (e: IKeyboardEvent) => {
this._onKeyUp.fire(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'compositionstart', (e: CompositionEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.compositionstart;
if (this._isDoingComposition) {
return;
}
this._isDoingComposition = true;
// In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled.
if (!browser.isEdgeOrIE) {
this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY);
}
this._onCompositionStart.fire();
}));
/**
* Deduce the typed input from a text area's value and the last observed state.
*/
const deduceInputFromTextAreaValue = (couldBeEmojiInput: boolean, couldBeTypingAtOffset0: boolean): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = TextAreaState.readFromTextArea(this._textArea);
return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput, couldBeTypingAtOffset0)];
};
/**
* Deduce the composition input from a string.
*/
const deduceComposition = (text: string): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = TextAreaState.selectedText(text);
const typeInput: ITypeData = {
text: newState.value,
replaceCharCnt: oldState.selectionEnd - oldState.selectionStart
};
return [newState, typeInput];
};
const compositionDataInValid = (locale: string): boolean => {
// https://github.com/Microsoft/monaco-editor/issues/339
// Multi-part Japanese compositions reset cursor in Edge/IE, Chinese and Korean IME don't have this issue.
// The reason that we can't use this path for all CJK IME is IE and Edge behave differently when handling Korean IME,
// which breaks this path of code.
if (browser.isEdgeOrIE && locale === 'ja') {
return true;
}
// https://github.com/Microsoft/monaco-editor/issues/545
// On IE11, we can't trust composition data when typing Chinese as IE11 doesn't emit correct
// events when users type numbers in IME.
// Chinese: zh-Hans-CN, zh-Hans-SG, zh-Hant-TW, zh-Hant-HK
if (browser.isIE && locale.indexOf('zh-Han') === 0) {
return true;
}
return false;
};
this._register(dom.addDisposableListener(textArea.domNode, 'compositionupdate', (e: CompositionEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.compositionupdate;
if (compositionDataInValid(e.locale)) {
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false, /*couldBeTypingAtOffset0*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
return;
}
const [newState, typeInput] = deduceComposition(e.data);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.compositionend;
if (compositionDataInValid(e.locale)) {
// https://github.com/Microsoft/monaco-editor/issues/339
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false, /*couldBeTypingAtOffset0*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
} else {
const [newState, typeInput] = deduceComposition(e.data);
this._textAreaState = newState;
this._onType.fire(typeInput);
}
// Due to isEdgeOrIE (where the textarea was not cleared initially) and isChrome (the textarea is not updated correctly when composition ends)
// we cannot assume the text at the end consists only of the composited text
if (browser.isEdgeOrIE || browser.isChrome) {
this._textAreaState = TextAreaState.readFromTextArea(this._textArea);
}
if (!this._isDoingComposition) {
return;
}
this._isDoingComposition = false;
this._onCompositionEnd.fire();
}));
this._register(dom.addDisposableListener(textArea.domNode, 'input', () => {
// We want to find out if this is the first `input` after a `focus`.
const previousEventWasFocus = (this._lastTextAreaEvent === TextAreaInputEventType.focus);
this._lastTextAreaEvent = TextAreaInputEventType.input;
// Pretend here we touched the text area, as the `input` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received input event');
if (this._isDoingComposition) {
return;
}
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/platform.isMacintosh, /*couldBeTypingAtOffset0*/previousEventWasFocus && platform.isMacintosh);
if (typeInput.replaceCharCnt === 0 && typeInput.text.length === 1 && strings.isHighSurrogate(typeInput.text.charCodeAt(0))) {
// Ignore invalid input but keep it around for next time
return;
}
this._textAreaState = newState;
if (this._nextCommand === ReadFromTextArea.Type) {
if (typeInput.text !== '') {
this._onType.fire(typeInput);
}
} else {
if (typeInput.text !== '') {
this._onPaste.fire({
text: typeInput.text
});
}
this._nextCommand = ReadFromTextArea.Type;
}
}));
// --- Clipboard operations
this._register(dom.addDisposableListener(textArea.domNode, 'cut', (e: ClipboardEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.cut;
// Pretend here we touched the text area, as the `cut` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received cut event');
this._ensureClipboardGetsEditorSelection(e);
this._asyncTriggerCut.schedule();
}));
this._register(dom.addDisposableListener(textArea.domNode, 'copy', (e: ClipboardEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.copy;
this._ensureClipboardGetsEditorSelection(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'paste', (e: ClipboardEvent) => {
this._lastTextAreaEvent = TextAreaInputEventType.paste;
// Pretend here we touched the text area, as the `paste` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received paste event');
if (ClipboardEventUtils.canUseTextData(e)) {
const pastePlainText = ClipboardEventUtils.getTextData(e);
if (pastePlainText !== '') {
this._onPaste.fire({
text: pastePlainText
});
}
} else {
if (this._textArea.getSelectionStart() !== this._textArea.getSelectionEnd()) {
// Clean up the textarea, to get a clean paste
this._setAndWriteTextAreaState('paste', TextAreaState.EMPTY);
}
this._nextCommand = ReadFromTextArea.Paste;
}
}));
this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => {
this._lastTextAreaEvent = TextAreaInputEventType.focus;
this._setHasFocus(true);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => {
this._lastTextAreaEvent = TextAreaInputEventType.blur;
this._setHasFocus(false);
}));
}