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>
</template>
<script lang="ts">
<script lang="ts" setup>
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import contains from '@/scripts/contains';
import { char2filePath } from '@/scripts/twemoji-base';
@ -67,8 +67,8 @@ for (const x of lib) {
for (const k of x.keywords) {
emjdb.push({
emoji: x.char,
name: k,
aliasOf: x.name,
name: k.toLowerCase(),
aliasOf: x.name.toLowerCase(),
url: char2filePath(x.char),
});
}
@ -83,7 +83,7 @@ const emojiDefinitions: EmojiDef[] = [];
for (const x of customEmojis) {
emojiDefinitions.push({
name: x.name,
name: x.name.toLowerCase(),
emoji: `:${x.name}:`,
url: x.url,
isCustomEmoji: true,
@ -92,8 +92,8 @@ for (const x of customEmojis) {
if (x.aliases) {
for (const alias of x.aliases) {
emojiDefinitions.push({
name: alias,
aliasOf: x.name,
name: alias.toLowerCase(),
aliasOf: x.name.toLowerCase(),
emoji: `:${x.name}:`,
url: x.url,
isCustomEmoji: true,
@ -107,15 +107,6 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
//#endregion
export default {
emojiDb,
emojiDefinitions,
emojilist,
customEmojis,
};
</script>
<script lang="ts" setup>
const props = defineProps<{
type: string;
q: string | null;
@ -226,7 +217,7 @@ function exec() {
}
} else if (props.type === 'emoji') {
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[];
return;
}
@ -234,23 +225,35 @@ function exec() {
const matched: EmojiDef[] = [];
const max = 30;
emojiDb.some(x => {
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
return matched.length === max;
});
// match emoji case insensitive
const q = props.q.toLowerCase();
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) {
emojiDb.some(x => {
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
return matched.length === max;
});
// there is still space left, now allow aliases too, but still starting with
matchingEmoji(x => x.name.startsWith(q));
}
if (matched.length < max) {
emojiDb.some(x => {
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
return matched.length === max;
});
// there is *still* space left, now just check anywhere in the name or alias
matchingEmoji(x => x.name.includes(q));
}
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>) {
//#region BIND
@ -42,23 +42,20 @@ export class Autocomplete {
}
/**
*
* Starts capturing text area input.
*/
public attach() {
this.textarea.addEventListener('input', this.onInput);
}
/**
*
* Stop capturing text area input.
*/
public detach() {
this.textarea.removeEventListener('input', this.onInput);
this.close();
}
/**
*
*/
private onInput() {
const caretPos = this.textarea.selectionStart;
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) {
if (type !== this.currentType) {
@ -137,14 +134,13 @@ export class Autocomplete {
this.opening = true;
this.currentType = type;
//#region サジェストを表示すべき位置を計算
// calculate where the suggestion should appear
const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
const rect = this.textarea.getBoundingClientRect();
const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
const y = rect.top + caretPosition.top - this.textarea.scrollTop;
//#endregion
if (this.suggestion) {
this.suggestion.x.value = x;
@ -182,7 +178,7 @@ export class Autocomplete {
}
/**
*
* Close suggestion.
*/
private close() {
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 }) {
this.close();
@ -210,15 +216,9 @@ export class Autocomplete {
const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
// 挿入
this.text = `${trimmedBefore}@${acct} ${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (acct.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
// add 2 for "@" and space
this.positionCursor(trimmedBefore.length + acct.length + 2);
} else if (type === 'hashtag') {
const source = this.text;
@ -226,15 +226,9 @@ export class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
const after = source.substr(caret);
// 挿入
this.text = `${trimmedBefore}#${value} ${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
// add 2 for "#" and space
this.positionCursor(trimmedBefore.length + value.length + 2);
} else if (type === 'emoji') {
const source = this.text;
@ -242,15 +236,8 @@ export class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
const after = source.substr(caret);
// 挿入
this.text = trimmedBefore + value + after;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + value.length;
this.textarea.setSelectionRange(pos, pos);
});
this.positionCursor(trimmedBefore.length + value.length);
} else if (type === 'mfmTag') {
const source = this.text;
@ -258,15 +245,8 @@ export class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
const after = source.substr(caret);
// 挿入
this.text = `${trimmedBefore}$[${value} ]${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 3);
this.textarea.setSelectionRange(pos, pos);
});
this.positionCursor(trimmedBefore.length + value.length + 3);
}
}
}