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>
|
</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;
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue