Merge pull request #5698 from ntomoya/fix/ime-middle-composition-suffix
Fix IME input when composing in the middle of the textarea
This commit is contained in:
@@ -233,5 +233,31 @@ describe('CompositionHelper', () => {
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('Should insert middle composition and subsequent input without appending existing trailing text', (done) => {
|
||||
textarea.value = '一二';
|
||||
// screenReaderMode keeps textarea content/selection for assistive technologies (eg. screen
|
||||
// readers), so the caret can be moved within the textarea (eg. via arrow keys) before
|
||||
// starting composition.
|
||||
textarea.selectionStart = 1;
|
||||
textarea.selectionEnd = 1;
|
||||
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: '一' });
|
||||
textarea.value = '一一二';
|
||||
// After the composed text is inserted, the caret typically moves to after it.
|
||||
textarea.selectionStart = 2;
|
||||
textarea.selectionEnd = 2;
|
||||
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
// Second character '1' (a non-composition character)
|
||||
textarea.value = '一一1二';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
assert.equal(handledText, '一1');
|
||||
done();
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,12 @@ export class CompositionHelper {
|
||||
*/
|
||||
private _compositionPosition: IPosition;
|
||||
|
||||
/**
|
||||
* Text that existed after the composing range when composition started.
|
||||
* This is used to avoid treating existing trailing text as new input.
|
||||
*/
|
||||
private _compositionSuffix: string;
|
||||
|
||||
/**
|
||||
* Whether a composition is in the process of being sent, setting this to false will cancel any
|
||||
* in-progress composition.
|
||||
@@ -57,6 +63,7 @@ export class CompositionHelper {
|
||||
this._isComposing = false;
|
||||
this._isSendingComposition = false;
|
||||
this._compositionPosition = { start: 0, end: 0 };
|
||||
this._compositionSuffix = '';
|
||||
this._dataAlreadySent = '';
|
||||
}
|
||||
|
||||
@@ -65,7 +72,13 @@ export class CompositionHelper {
|
||||
*/
|
||||
public compositionstart(): void {
|
||||
this._isComposing = true;
|
||||
this._compositionPosition.start = this._textarea.value.length;
|
||||
// It's important to use the selection here instead of textarea length to avoid conflicts with
|
||||
// screen reader mode
|
||||
const start = this._textarea.selectionStart ?? this._textarea.value.length;
|
||||
const end = this._textarea.selectionEnd ?? start;
|
||||
this._compositionPosition.start = Math.min(start, end);
|
||||
this._compositionPosition.end = Math.max(start, end);
|
||||
this._compositionSuffix = this._textarea.value.substring(this._compositionPosition.end);
|
||||
this._compositionView.textContent = '';
|
||||
this._dataAlreadySent = '';
|
||||
this._compositionView.classList.add('active');
|
||||
@@ -81,7 +94,8 @@ export class CompositionHelper {
|
||||
this._compositionView.textContent = `\u200E${ev.data}\u200E`;
|
||||
this.updateCompositionElements();
|
||||
setTimeout(() => {
|
||||
this._compositionPosition.end = this._textarea.value.length;
|
||||
const end = this._textarea.selectionEnd ?? this._textarea.value.length;
|
||||
this._compositionPosition.end = Math.max( this._compositionPosition.start, end);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -148,6 +162,7 @@ export class CompositionHelper {
|
||||
start: this._compositionPosition.start,
|
||||
end: this._compositionPosition.end
|
||||
};
|
||||
const currentCompositionSuffix = this._compositionSuffix;
|
||||
|
||||
// Since composition* events happen before the changes take place in the textarea on most
|
||||
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
|
||||
@@ -171,10 +186,14 @@ export class CompositionHelper {
|
||||
// if a new composition has started.
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
|
||||
} else {
|
||||
// Don't use the end position here in order to pick up any characters after the
|
||||
// composition has finished, for example when typing a non-composition character
|
||||
// (eg. 2) after a composition character.
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start);
|
||||
// Keep support for non-composition characters typed immediately after composition end
|
||||
// while avoiding re-sending the trailing text that was already present
|
||||
// before composition started.
|
||||
const value = this._textarea.value;
|
||||
const valueEnd = currentCompositionSuffix.length > 0 && value.endsWith(currentCompositionSuffix)
|
||||
? value.length - currentCompositionSuffix.length
|
||||
: value.length;
|
||||
input = value.substring(currentCompositionPosition.start, Math.max(currentCompositionPosition.start, valueEnd));
|
||||
}
|
||||
if (input.length > 0) {
|
||||
this._coreService.triggerDataEvent(input, true);
|
||||
|
||||
Reference in New Issue
Block a user