forked from FoundKeyGang/FoundKey
client: refactor emoji autocomplete & make case insensitive
Changelog: Changed
This commit is contained in:
parent
cdb8922336
commit
13fda0c9c7
2 changed files with 54 additions and 71 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue