Support backspace after emoji completion

Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
Fedor Indutny 2024-05-28 19:49:49 -07:00 committed by GitHub
parent 85a75cb28f
commit 4ec69ee3a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 62 additions and 46 deletions

View file

@ -66,7 +66,7 @@ export class AutoSubstituteAsciiEmojis {
} }
onTextChange(): void { onTextChange(): void {
if (!window.storage.get('autoConvertEmoji', false)) { if (!window.storage.get('autoConvertEmoji', true)) {
return; return;
} }

View file

@ -27,6 +27,14 @@ type EmojiPickerOptions = {
search: SearchFnType; search: SearchFnType;
}; };
export type InsertEmojiOptionsType = Readonly<{
shortName: string;
index: number;
range: number;
withTrailingSpace?: boolean;
justPressedColon?: boolean;
}>;
export class EmojiCompletion { export class EmojiCompletion {
results: Array<string>; results: Array<string>;
@ -84,10 +92,13 @@ export class EmojiCompletion {
() => this.onTextChange(true) () => this.onTextChange(true)
); );
this.quill.on( const debouncedOnTextChange = _.debounce(() => this.onTextChange(), 100);
'text-change',
_.debounce(() => this.onTextChange(), 100) this.quill.on('text-change', (_now, _before, source) => {
); if (source === 'user') {
debouncedOnTextChange();
}
});
this.quill.on('selection-change', this.onSelectionChange.bind(this)); this.quill.on('selection-change', this.onSelectionChange.bind(this));
} }
@ -139,11 +150,12 @@ export class EmojiCompletion {
if (isShortName(leftTokenText)) { if (isShortName(leftTokenText)) {
const numberOfColons = isSelfClosing ? 2 : 1; const numberOfColons = isSelfClosing ? 2 : 1;
this.insertEmoji( this.insertEmoji({
leftTokenText, shortName: leftTokenText,
range.index - leftTokenText.length - numberOfColons, index: range.index - leftTokenText.length - numberOfColons,
leftTokenText.length + numberOfColons range: leftTokenText.length + numberOfColons,
); justPressedColon,
});
return INTERCEPT; return INTERCEPT;
} }
this.reset(); this.reset();
@ -155,11 +167,12 @@ export class EmojiCompletion {
const tokenText = leftTokenText + rightTokenText; const tokenText = leftTokenText + rightTokenText;
if (isShortName(tokenText)) { if (isShortName(tokenText)) {
this.insertEmoji( this.insertEmoji({
tokenText, shortName: tokenText,
range.index - leftTokenText.length - 1, index: range.index - leftTokenText.length - 1,
tokenText.length + 2 range: tokenText.length + 2,
); justPressedColon,
});
return INTERCEPT; return INTERCEPT;
} }
} }
@ -212,27 +225,33 @@ export class EmojiCompletion {
const [, tokenText] = tokenTextMatch; const [, tokenText] = tokenTextMatch;
this.insertEmoji( this.insertEmoji({
emoji, shortName: emoji,
range.index - tokenText.length - 1, index: range.index - tokenText.length - 1,
tokenText.length + 1, range: tokenText.length + 1,
true withTrailingSpace: true,
); });
} }
insertEmoji( insertEmoji({
shortName: string, shortName,
index: number, index,
range: number, range,
withTrailingSpace = false withTrailingSpace = false,
): void { justPressedColon = false,
}: InsertEmojiOptionsType): void {
const emoji = convertShortName(shortName, this.options.skinTone); const emoji = convertShortName(shortName, this.options.skinTone);
let source = this.quill.getText(index, range);
if (justPressedColon) {
source += ':';
}
const delta = new Delta() const delta = new Delta()
.retain(index) .retain(index)
.delete(range) .delete(range)
.insert({ .insert({
emoji: { value: emoji }, emoji: { value: emoji, source },
}); });
if (withTrailingSpace) { if (withTrailingSpace) {

View file

@ -5,6 +5,7 @@ import { assert } from 'chai';
import sinon from 'sinon'; import sinon from 'sinon';
import { EmojiCompletion } from '../../../quill/emoji/completion'; import { EmojiCompletion } from '../../../quill/emoji/completion';
import type { InsertEmojiOptionsType } from '../../../quill/emoji/completion';
import { createSearch } from '../../../components/emoji/lib'; import { createSearch } from '../../../components/emoji/lib';
describe('emojiCompletion', () => { describe('emojiCompletion', () => {
@ -15,6 +16,7 @@ describe('emojiCompletion', () => {
beforeEach(function (this: Mocha.Context) { beforeEach(function (this: Mocha.Context) {
mockQuill = { mockQuill = {
getLeaf: sinon.stub(), getLeaf: sinon.stub(),
getText: sinon.stub(),
getSelection: sinon.stub(), getSelection: sinon.stub(),
keyboard: { keyboard: {
addBinding: sinon.stub(), addBinding: sinon.stub(),
@ -55,10 +57,7 @@ describe('emojiCompletion', () => {
}); });
describe('onTextChange', () => { describe('onTextChange', () => {
let insertEmojiStub: sinon.SinonStub< let insertEmojiStub: sinon.SinonStub<[InsertEmojiOptionsType], void>;
[string, number, number, (boolean | undefined)?],
void
>;
beforeEach(() => { beforeEach(() => {
emojiCompletion.results = ['joy']; emojiCompletion.results = ['joy'];
@ -194,9 +193,9 @@ describe('emojiCompletion', () => {
}); });
it('inserts the emoji at the current cursor position', () => { it('inserts the emoji at the current cursor position', () => {
const [emoji, index, range] = insertEmojiStub.args[0]; const [{ shortName, index, range }] = insertEmojiStub.args[0];
assert.equal(emoji, 'smile'); assert.equal(shortName, 'smile');
assert.equal(index, 0); assert.equal(index, 0);
assert.equal(range, 7); assert.equal(range, 7);
}); });
@ -223,9 +222,9 @@ describe('emojiCompletion', () => {
}); });
it('inserts the emoji at the current cursor position', () => { it('inserts the emoji at the current cursor position', () => {
const [emoji, index, range] = insertEmojiStub.args[0]; const [{ shortName, index, range }] = insertEmojiStub.args[0];
assert.equal(emoji, 'smile'); assert.equal(shortName, 'smile');
assert.equal(index, 7); assert.equal(index, 7);
assert.equal(range, 7); assert.equal(range, 7);
}); });
@ -283,9 +282,9 @@ describe('emojiCompletion', () => {
}); });
it('inserts the emoji at the current cursor position', () => { it('inserts the emoji at the current cursor position', () => {
const [emoji, index, range] = insertEmojiStub.args[0]; const [{ shortName, index, range }] = insertEmojiStub.args[0];
assert.equal(emoji, 'smile'); assert.equal(shortName, 'smile');
assert.equal(index, 0); assert.equal(index, 0);
assert.equal(range, validEmoji.length); assert.equal(range, validEmoji.length);
}); });
@ -332,9 +331,9 @@ describe('emojiCompletion', () => {
}); });
it('inserts the emoji at the current cursor position', () => { it('inserts the emoji at the current cursor position', () => {
const [emoji, index, range] = insertEmojiStub.args[0]; const [{ shortName, index, range }] = insertEmojiStub.args[0];
assert.equal(emoji, 'smile'); assert.equal(shortName, 'smile');
assert.equal(index, 0); assert.equal(index, 0);
assert.equal(range, 6); assert.equal(range, 6);
}); });
@ -347,10 +346,7 @@ describe('emojiCompletion', () => {
}); });
describe('completeEmoji', () => { describe('completeEmoji', () => {
let insertEmojiStub: sinon.SinonStub< let insertEmojiStub: sinon.SinonStub<[InsertEmojiOptionsType], void>;
[string, number, number, (boolean | undefined)?],
void
>;
beforeEach(() => { beforeEach(() => {
emojiCompletion.results = ['smile', 'smile_cat']; emojiCompletion.results = ['smile', 'smile_cat'];
@ -377,9 +373,10 @@ describe('emojiCompletion', () => {
}); });
it('inserts the currently selected emoji at the current cursor position', () => { it('inserts the currently selected emoji at the current cursor position', () => {
const [emoji, insertIndex, range] = insertEmojiStub.args[0]; const [{ shortName, index: insertIndex, range }] =
insertEmojiStub.args[0];
assert.equal(emoji, 'smile_cat'); assert.equal(shortName, 'smile_cat');
assert.equal(insertIndex, 0); assert.equal(insertIndex, 0);
assert.equal(range, text.length); assert.equal(range, text.length);
}); });