client: refactor emoji autocomplete & make case insensitive

Changelog: Changed
This commit is contained in:
Johann150 2022-11-29 21:13:20 +01:00
parent cdb8922336
commit 13fda0c9c7
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
2 changed files with 54 additions and 71 deletions

View file

@ -33,7 +33,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import contains from '@/scripts/contains'; import contains from '@/scripts/contains';
import { char2filePath } from '@/scripts/twemoji-base'; import { char2filePath } from '@/scripts/twemoji-base';
@ -67,8 +67,8 @@ for (const x of lib) {
for (const k of x.keywords) { for (const k of x.keywords) {
emjdb.push({ emjdb.push({
emoji: x.char, emoji: x.char,
name: k, name: k.toLowerCase(),
aliasOf: x.name, aliasOf: x.name.toLowerCase(),
url: char2filePath(x.char), url: char2filePath(x.char),
}); });
} }
@ -83,7 +83,7 @@ const emojiDefinitions: EmojiDef[] = [];
for (const x of customEmojis) { for (const x of customEmojis) {
emojiDefinitions.push({ emojiDefinitions.push({
name: x.name, name: x.name.toLowerCase(),
emoji: `:${x.name}:`, emoji: `:${x.name}:`,
url: x.url, url: x.url,
isCustomEmoji: true, isCustomEmoji: true,
@ -92,8 +92,8 @@ for (const x of customEmojis) {
if (x.aliases) { if (x.aliases) {
for (const alias of x.aliases) { for (const alias of x.aliases) {
emojiDefinitions.push({ emojiDefinitions.push({
name: alias, name: alias.toLowerCase(),
aliasOf: x.name, aliasOf: x.name.toLowerCase(),
emoji: `:${x.name}:`, emoji: `:${x.name}:`,
url: x.url, url: x.url,
isCustomEmoji: true, isCustomEmoji: true,
@ -107,15 +107,6 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
const emojiDb = markRaw(emojiDefinitions.concat(emjdb)); const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
//#endregion //#endregion
export default {
emojiDb,
emojiDefinitions,
emojilist,
customEmojis,
};
</script>
<script lang="ts" setup>
const props = defineProps<{ const props = defineProps<{
type: string; type: string;
q: string | null; q: string | null;
@ -226,7 +217,7 @@ function exec() {
} }
} else if (props.type === 'emoji') { } else if (props.type === 'emoji') {
if (!props.q || props.q === '') { if (!props.q || props.q === '') {
// 使 // suggest recently used emoji
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
return; return;
} }
@ -234,23 +225,35 @@ function exec() {
const matched: EmojiDef[] = []; const matched: EmojiDef[] = [];
const max = 30; const max = 30;
emojiDb.some(x => { // match emoji case insensitive
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x); const q = props.q.toLowerCase();
return matched.length === max;
}); const matchingEmoji = (matcher) => {
for (const x of emojiDb) {
if (
matcher(x)
// make sure an emoji isnt shown twice (because of an alias)
&& !matched.some(y => y.emoji === x.emoji)
) {
matched.push(x);
// if this brings us to the max allowed, stop checking
if (matched.length === max) break;
}
}
}
// at first only check starting and dont allow aliases
matchingEmoji(x => x.name.startsWith(q) && !x.aliasOf);
if (matched.length < max) { if (matched.length < max) {
emojiDb.some(x => { // there is still space left, now allow aliases too, but still starting with
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x); matchingEmoji(x => x.name.startsWith(q));
return matched.length === max;
});
} }
if (matched.length < max) { if (matched.length < max) {
emojiDb.some(x => { // there is *still* space left, now just check anywhere in the name or alias
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x); matchingEmoji(x => x.name.includes(q));
return matched.length === max;
});
} }
emojis.value = matched; emojis.value = matched;

View file

@ -24,7 +24,7 @@ export class Autocomplete {
} }
/** /**
* * Initialize the object by giving it the targeted text area.
*/ */
constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) { constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
//#region BIND //#region BIND
@ -42,23 +42,20 @@ export class Autocomplete {
} }
/** /**
* * Starts capturing text area input.
*/ */
public attach() { public attach() {
this.textarea.addEventListener('input', this.onInput); this.textarea.addEventListener('input', this.onInput);
} }
/** /**
* * Stop capturing text area input.
*/ */
public detach() { public detach() {
this.textarea.removeEventListener('input', this.onInput); this.textarea.removeEventListener('input', this.onInput);
this.close(); this.close();
} }
/**
*
*/
private onInput() { private onInput() {
const caretPos = this.textarea.selectionStart; const caretPos = this.textarea.selectionStart;
const text = this.text.substr(0, caretPos).split('\n').pop()!; const text = this.text.substr(0, caretPos).split('\n').pop()!;
@ -127,7 +124,7 @@ export class Autocomplete {
} }
/** /**
* * Show suggestions.
*/ */
private async open(type: string, q: string | null) { private async open(type: string, q: string | null) {
if (type !== this.currentType) { if (type !== this.currentType) {
@ -137,14 +134,13 @@ export class Autocomplete {
this.opening = true; this.opening = true;
this.currentType = type; this.currentType = type;
//#region サジェストを表示すべき位置を計算 // calculate where the suggestion should appear
const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
const rect = this.textarea.getBoundingClientRect(); const rect = this.textarea.getBoundingClientRect();
const x = rect.left + caretPosition.left - this.textarea.scrollLeft; const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
const y = rect.top + caretPosition.top - this.textarea.scrollTop; const y = rect.top + caretPosition.top - this.textarea.scrollTop;
//#endregion
if (this.suggestion) { if (this.suggestion) {
this.suggestion.x.value = x; this.suggestion.x.value = x;
@ -182,7 +178,7 @@ export class Autocomplete {
} }
/** /**
* * Close suggestion.
*/ */
private close() { private close() {
if (this.suggestion == null) return; if (this.suggestion == null) return;
@ -194,7 +190,17 @@ export class Autocomplete {
} }
/** /**
* * Positions the cursor within the given text area.
*/
private positionCursor(pos) {
nextTick(() => {
this.textarea.focus();
this.textarea.setSelectionRange(pos, pos);
});
}
/**
* Write the suggestion to the textarea.
*/ */
private complete({ type, value }) { private complete({ type, value }) {
this.close(); this.close();
@ -210,15 +216,9 @@ export class Autocomplete {
const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
// 挿入
this.text = `${trimmedBefore}@${acct} ${after}`; this.text = `${trimmedBefore}@${acct} ${after}`;
// add 2 for "@" and space
// キャレットを戻す this.positionCursor(trimmedBefore.length + acct.length + 2);
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (acct.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'hashtag') { } else if (type === 'hashtag') {
const source = this.text; const source = this.text;
@ -226,15 +226,9 @@ export class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf('#')); const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
const after = source.substr(caret); const after = source.substr(caret);
// 挿入
this.text = `${trimmedBefore}#${value} ${after}`; this.text = `${trimmedBefore}#${value} ${after}`;
// add 2 for "#" and space
// キャレットを戻す this.positionCursor(trimmedBefore.length + value.length + 2);
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'emoji') { } else if (type === 'emoji') {
const source = this.text; const source = this.text;
@ -242,15 +236,8 @@ export class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf(':')); const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
const after = source.substr(caret); const after = source.substr(caret);
// 挿入
this.text = trimmedBefore + value + after; this.text = trimmedBefore + value + after;
this.positionCursor(trimmedBefore.length + value.length);
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + value.length;
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'mfmTag') { } else if (type === 'mfmTag') {
const source = this.text; const source = this.text;
@ -258,15 +245,8 @@ export class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf('$')); const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
const after = source.substr(caret); const after = source.substr(caret);
// 挿入
this.text = `${trimmedBefore}$[${value} ]${after}`; this.text = `${trimmedBefore}$[${value} ]${after}`;
this.positionCursor(trimmedBefore.length + value.length + 3);
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 3);
this.textarea.setSelectionRange(pos, pos);
});
} }
} }
} }