2023-07-27 05:31:52 +00:00
|
|
|
|
<!--
|
|
|
|
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
-->
|
|
|
|
|
|
2018-02-20 11:29:52 +00:00
|
|
|
|
<template>
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<div ref="rootEl" :class="$style.root" class="_popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
|
|
|
|
|
<ol v-if="type === 'user'" ref="suggests" :class="$style.list">
|
|
|
|
|
<li v-for="user in users" tabindex="-1" :class="$style.item" @click="complete(type, user)" @keydown="onKeydown">
|
|
|
|
|
<img :class="$style.avatar" :src="user.avatarUrl"/>
|
|
|
|
|
<span :class="$style.userName">
|
2023-02-08 11:07:19 +00:00
|
|
|
|
<MkUserName :key="user.id" :user="user"/>
|
2018-12-06 01:02:04 +00:00
|
|
|
|
</span>
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<span>@{{ acct(user) }}</span>
|
2018-02-20 11:29:52 +00:00
|
|
|
|
</li>
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
|
2018-02-20 11:29:52 +00:00
|
|
|
|
</ol>
|
2023-06-01 08:19:46 +00:00
|
|
|
|
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list">
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
|
2018-07-17 22:19:24 +00:00
|
|
|
|
<span class="name">{{ hashtag }}</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ol>
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list">
|
|
|
|
|
<li v-for="emoji in emojis" :key="emoji.emoji" :class="$style.item" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
2023-02-03 21:21:36 +00:00
|
|
|
|
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji"/>
|
|
|
|
|
<MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/>
|
2022-07-05 02:21:59 +00:00
|
|
|
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
|
2022-12-30 04:02:18 +00:00
|
|
|
|
<span v-else v-text="emoji.name"></span>
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<span v-if="emoji.aliasOf" :class="$style.emojiAlias">({{ emoji.aliasOf }})</span>
|
2018-02-24 16:55:49 +00:00
|
|
|
|
</li>
|
|
|
|
|
</ol>
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<ol v-else-if="mfmTags.length > 0" ref="suggests" :class="$style.list">
|
|
|
|
|
<li v-for="tag in mfmTags" tabindex="-1" :class="$style.item" @click="complete(type, tag)" @keydown="onKeydown">
|
|
|
|
|
<span>{{ tag }}</span>
|
2021-09-25 17:55:11 +00:00
|
|
|
|
</li>
|
|
|
|
|
</ol>
|
2018-02-20 11:29:52 +00:00
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts">
|
2023-01-16 09:39:58 +00:00
|
|
|
|
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
2023-01-03 01:12:37 +00:00
|
|
|
|
import sanitizeHtml from 'sanitize-html';
|
2023-09-19 07:37:43 +00:00
|
|
|
|
import contains from '@/scripts/contains.js';
|
2024-01-04 19:42:25 +00:00
|
|
|
|
import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js';
|
2023-09-19 07:37:43 +00:00
|
|
|
|
import { acct } from '@/filters/user.js';
|
|
|
|
|
import * as os from '@/os.js';
|
|
|
|
|
import { defaultStore } from '@/store.js';
|
|
|
|
|
import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
|
|
|
|
import { i18n } from '@/i18n.js';
|
|
|
|
|
import { miLocalStorage } from '@/local-storage.js';
|
|
|
|
|
import { customEmojis } from '@/custom-emojis.js';
|
2023-11-17 04:09:56 +00:00
|
|
|
|
import { MFM_TAGS } from '@/const.js';
|
2018-02-20 11:29:52 +00:00
|
|
|
|
|
2018-11-01 12:28:39 +00:00
|
|
|
|
type EmojiDef = {
|
|
|
|
|
emoji: string;
|
|
|
|
|
name: string;
|
2022-12-30 03:00:50 +00:00
|
|
|
|
url: string;
|
2018-11-01 12:28:39 +00:00
|
|
|
|
aliasOf?: string;
|
2022-12-30 03:00:50 +00:00
|
|
|
|
} | {
|
|
|
|
|
emoji: string;
|
|
|
|
|
name: string;
|
|
|
|
|
aliasOf?: string;
|
|
|
|
|
isCustomEmoji?: true;
|
2018-11-01 12:28:39 +00:00
|
|
|
|
};
|
|
|
|
|
|
2019-09-21 12:31:38 +00:00
|
|
|
|
const lib = emojilist.filter(x => x.category !== 'flags');
|
2018-04-09 09:52:29 +00:00
|
|
|
|
|
2023-01-16 09:39:58 +00:00
|
|
|
|
const emojiDb = computed(() => {
|
2023-01-16 10:56:43 +00:00
|
|
|
|
//#region Unicode Emoji
|
2024-01-04 19:42:25 +00:00
|
|
|
|
const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : defaultStore.reactiveState.emojiStyle.value === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath;
|
2023-01-16 09:39:58 +00:00
|
|
|
|
|
|
|
|
|
const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
|
|
|
|
|
emoji: x.char,
|
|
|
|
|
name: x.name,
|
|
|
|
|
url: char2path(x.char),
|
|
|
|
|
}));
|
|
|
|
|
|
2023-06-01 08:10:53 +00:00
|
|
|
|
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
|
|
|
|
for (const [emoji, keywords] of Object.entries(index)) {
|
|
|
|
|
for (const k of keywords) {
|
|
|
|
|
unicodeEmojiDB.push({
|
|
|
|
|
emoji: emoji,
|
|
|
|
|
name: k,
|
|
|
|
|
aliasOf: getEmojiName(emoji)!,
|
|
|
|
|
url: char2path(emoji),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-16 09:39:58 +00:00
|
|
|
|
unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length);
|
2023-01-16 10:56:43 +00:00
|
|
|
|
//#endregion
|
2023-01-16 09:39:58 +00:00
|
|
|
|
|
2023-01-16 10:56:43 +00:00
|
|
|
|
//#region Custom Emoji
|
2023-01-16 09:39:58 +00:00
|
|
|
|
const customEmojiDB: EmojiDef[] = [];
|
|
|
|
|
|
|
|
|
|
for (const x of customEmojis.value) {
|
|
|
|
|
customEmojiDB.push({
|
|
|
|
|
name: x.name,
|
|
|
|
|
emoji: `:${x.name}:`,
|
|
|
|
|
isCustomEmoji: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (x.aliases) {
|
|
|
|
|
for (const alias of x.aliases) {
|
|
|
|
|
customEmojiDB.push({
|
|
|
|
|
name: alias,
|
|
|
|
|
aliasOf: x.name,
|
|
|
|
|
emoji: `:${x.name}:`,
|
|
|
|
|
isCustomEmoji: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
2021-08-12 03:41:57 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-16 09:39:58 +00:00
|
|
|
|
customEmojiDB.sort((a, b) => a.name.length - b.name.length);
|
2023-01-16 10:56:43 +00:00
|
|
|
|
//#endregion
|
2021-08-12 03:41:57 +00:00
|
|
|
|
|
2023-01-26 06:48:12 +00:00
|
|
|
|
return markRaw([...customEmojiDB, ...unicodeEmojiDB]);
|
2023-01-16 09:39:58 +00:00
|
|
|
|
});
|
2021-08-12 03:41:57 +00:00
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
export default {
|
|
|
|
|
emojiDb,
|
|
|
|
|
emojilist,
|
|
|
|
|
};
|
|
|
|
|
</script>
|
2018-07-17 22:19:24 +00:00
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
type: string;
|
|
|
|
|
q: string | null;
|
|
|
|
|
textarea: HTMLTextAreaElement;
|
|
|
|
|
close: () => void;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
2022-04-28 01:56:18 +00:00
|
|
|
|
(event: 'done', value: { type: string; value: any }): void;
|
|
|
|
|
(event: 'closed'): void;
|
2022-01-10 15:05:18 +00:00
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const suggests = ref<Element>();
|
2023-01-03 01:12:37 +00:00
|
|
|
|
const rootEl = shallowRef<HTMLDivElement>();
|
2022-01-10 15:05:18 +00:00
|
|
|
|
|
|
|
|
|
const fetching = ref(true);
|
|
|
|
|
const users = ref<any[]>([]);
|
|
|
|
|
const hashtags = ref<any[]>([]);
|
|
|
|
|
const emojis = ref<(EmojiDef)[]>([]);
|
|
|
|
|
const items = ref<Element[] | HTMLCollection>([]);
|
|
|
|
|
const mfmTags = ref<string[]>([]);
|
|
|
|
|
const select = ref(-1);
|
|
|
|
|
const zIndex = os.claimZIndex('high');
|
|
|
|
|
|
|
|
|
|
function complete(type: string, value: any) {
|
|
|
|
|
emit('done', { type, value });
|
|
|
|
|
emit('closed');
|
|
|
|
|
if (type === 'emoji') {
|
|
|
|
|
let recents = defaultStore.state.recentlyUsedEmojis;
|
2022-04-28 01:56:18 +00:00
|
|
|
|
recents = recents.filter((emoji: any) => emoji !== value);
|
2022-01-10 15:05:18 +00:00
|
|
|
|
recents.unshift(value);
|
|
|
|
|
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-17 22:19:24 +00:00
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
function setPosition() {
|
|
|
|
|
if (!rootEl.value) return;
|
|
|
|
|
if (props.x + rootEl.value.offsetWidth > window.innerWidth) {
|
|
|
|
|
rootEl.value.style.left = (window.innerWidth - rootEl.value.offsetWidth) + 'px';
|
|
|
|
|
} else {
|
|
|
|
|
rootEl.value.style.left = `${props.x}px`;
|
|
|
|
|
}
|
|
|
|
|
if (props.y + rootEl.value.offsetHeight > window.innerHeight) {
|
|
|
|
|
rootEl.value.style.top = (props.y - rootEl.value.offsetHeight) + 'px';
|
|
|
|
|
rootEl.value.style.marginTop = '0';
|
|
|
|
|
} else {
|
|
|
|
|
rootEl.value.style.top = props.y + 'px';
|
|
|
|
|
rootEl.value.style.marginTop = 'calc(1em + 8px)';
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-31 22:38:40 +00:00
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
function exec() {
|
|
|
|
|
select.value = -1;
|
|
|
|
|
if (suggests.value) {
|
|
|
|
|
for (const el of Array.from(items.value)) {
|
|
|
|
|
el.removeAttribute('data-selected');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (props.type === 'user') {
|
|
|
|
|
if (!props.q) {
|
|
|
|
|
users.value = [];
|
|
|
|
|
fetching.value = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-02-20 11:29:52 +00:00
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
const cacheKey = `autocomplete:user:${props.q}`;
|
|
|
|
|
const cache = sessionStorage.getItem(cacheKey);
|
|
|
|
|
|
|
|
|
|
if (cache) {
|
2022-03-02 09:52:49 +00:00
|
|
|
|
users.value = JSON.parse(cache);
|
2022-01-10 15:05:18 +00:00
|
|
|
|
fetching.value = false;
|
|
|
|
|
} else {
|
|
|
|
|
os.api('users/search-by-username-and-host', {
|
|
|
|
|
username: props.q,
|
|
|
|
|
limit: 10,
|
2022-12-22 07:01:59 +00:00
|
|
|
|
detail: false,
|
2022-01-10 15:05:18 +00:00
|
|
|
|
}).then(searchedUsers => {
|
|
|
|
|
users.value = searchedUsers as any[];
|
|
|
|
|
fetching.value = false;
|
|
|
|
|
// キャッシュ
|
|
|
|
|
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else if (props.type === 'hashtag') {
|
2022-03-01 12:36:20 +00:00
|
|
|
|
if (!props.q || props.q === '') {
|
2023-02-22 06:28:17 +00:00
|
|
|
|
hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]');
|
2022-01-10 15:05:18 +00:00
|
|
|
|
fetching.value = false;
|
|
|
|
|
} else {
|
|
|
|
|
const cacheKey = `autocomplete:hashtag:${props.q}`;
|
|
|
|
|
const cache = sessionStorage.getItem(cacheKey);
|
|
|
|
|
if (cache) {
|
|
|
|
|
const hashtags = JSON.parse(cache);
|
|
|
|
|
hashtags.value = hashtags;
|
|
|
|
|
fetching.value = false;
|
|
|
|
|
} else {
|
|
|
|
|
os.api('hashtags/search', {
|
|
|
|
|
query: props.q,
|
2022-12-22 07:01:59 +00:00
|
|
|
|
limit: 30,
|
2022-01-10 15:05:18 +00:00
|
|
|
|
}).then(searchedHashtags => {
|
|
|
|
|
hashtags.value = searchedHashtags as any[];
|
|
|
|
|
fetching.value = false;
|
|
|
|
|
// キャッシュ
|
|
|
|
|
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (props.type === 'emoji') {
|
2022-03-01 12:36:20 +00:00
|
|
|
|
if (!props.q || props.q === '') {
|
2022-01-10 15:05:18 +00:00
|
|
|
|
// 最近使った絵文字をサジェスト
|
2023-01-16 09:39:58 +00:00
|
|
|
|
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
2022-01-10 15:05:18 +00:00
|
|
|
|
return;
|
2018-12-11 11:36:55 +00:00
|
|
|
|
}
|
2018-02-20 11:29:52 +00:00
|
|
|
|
|
2023-12-23 01:09:23 +00:00
|
|
|
|
emojis.value = emojiAutoComplete(props.q.toLowerCase(), emojiDb.value);
|
2022-01-10 15:05:18 +00:00
|
|
|
|
} else if (props.type === 'mfmTag') {
|
2022-03-01 12:36:20 +00:00
|
|
|
|
if (!props.q || props.q === '') {
|
2022-01-10 15:05:18 +00:00
|
|
|
|
mfmTags.value = MFM_TAGS;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-01 12:36:20 +00:00
|
|
|
|
mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q ?? ''));
|
2022-01-10 15:05:18 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 23:13:51 +00:00
|
|
|
|
type EmojiScore = { emoji: EmojiDef, score: number };
|
|
|
|
|
|
|
|
|
|
function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
|
|
|
|
|
if (!query) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const matched = new Map<string, EmojiScore>();
|
|
|
|
|
|
|
|
|
|
// 前方一致(エイリアスなし)
|
|
|
|
|
emojiDb.some(x => {
|
2023-12-23 01:09:23 +00:00
|
|
|
|
if (x.name.toLowerCase().startsWith(query) && !x.aliasOf) {
|
2023-11-23 21:37:06 +00:00
|
|
|
|
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
2023-11-22 23:13:51 +00:00
|
|
|
|
}
|
|
|
|
|
return matched.size === max;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 前方一致(エイリアス込み)
|
|
|
|
|
if (matched.size < max) {
|
|
|
|
|
emojiDb.some(x => {
|
2023-12-23 01:09:23 +00:00
|
|
|
|
if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
|
2023-11-23 21:37:06 +00:00
|
|
|
|
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
|
2023-11-22 23:13:51 +00:00
|
|
|
|
}
|
|
|
|
|
return matched.size === max;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 部分一致(エイリアス込み)
|
|
|
|
|
if (matched.size < max) {
|
|
|
|
|
emojiDb.some(x => {
|
2023-12-23 01:09:23 +00:00
|
|
|
|
if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) {
|
2023-11-23 21:37:06 +00:00
|
|
|
|
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
|
2023-11-22 23:13:51 +00:00
|
|
|
|
}
|
|
|
|
|
return matched.size === max;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-23 21:37:06 +00:00
|
|
|
|
// 簡易あいまい検索(3文字以上)
|
|
|
|
|
if (matched.size < max && query.length > 3) {
|
2023-11-22 23:13:51 +00:00
|
|
|
|
const queryChars = [...query];
|
|
|
|
|
const hitEmojis = new Map<string, EmojiScore>();
|
|
|
|
|
|
|
|
|
|
for (const x of emojiDb) {
|
2023-11-23 21:37:06 +00:00
|
|
|
|
// 文字列の位置を進めながら、クエリの文字を順番に探す
|
|
|
|
|
|
|
|
|
|
let pos = 0;
|
|
|
|
|
let hit = 0;
|
|
|
|
|
for (const c of queryChars) {
|
2023-12-23 01:09:23 +00:00
|
|
|
|
pos = x.name.toLowerCase().indexOf(c, pos);
|
2023-11-23 21:37:06 +00:00
|
|
|
|
if (pos <= -1) break;
|
|
|
|
|
hit++;
|
2023-11-22 23:13:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-23 21:37:06 +00:00
|
|
|
|
// 半分以上の文字が含まれていればヒットとする
|
|
|
|
|
if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
|
|
|
|
|
hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
|
2023-11-22 23:13:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分)
|
|
|
|
|
[...hitEmojis.values()]
|
|
|
|
|
.sort((x, y) => y.score - x.score)
|
|
|
|
|
.slice(0, 6)
|
|
|
|
|
.forEach(it => matched.set(it.emoji.name, it));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [...matched.values()]
|
|
|
|
|
.sort((x, y) => y.score - x.score)
|
|
|
|
|
.slice(0, max)
|
|
|
|
|
.map(it => it.emoji);
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-28 01:56:18 +00:00
|
|
|
|
function onMousedown(event: Event) {
|
|
|
|
|
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
|
2022-01-10 15:05:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-04-28 01:56:18 +00:00
|
|
|
|
function onKeydown(event: KeyboardEvent) {
|
2022-01-10 15:05:18 +00:00
|
|
|
|
const cancel = () => {
|
2022-04-28 01:56:18 +00:00
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
2022-01-10 15:05:18 +00:00
|
|
|
|
};
|
|
|
|
|
|
2022-04-28 01:56:18 +00:00
|
|
|
|
switch (event.key) {
|
2022-01-10 15:05:18 +00:00
|
|
|
|
case 'Enter':
|
|
|
|
|
if (select.value !== -1) {
|
|
|
|
|
cancel();
|
|
|
|
|
(items.value[select.value] as any).click();
|
2020-01-31 22:38:40 +00:00
|
|
|
|
} else {
|
2022-01-10 15:05:18 +00:00
|
|
|
|
props.close();
|
2020-01-31 22:38:40 +00:00
|
|
|
|
}
|
2022-01-10 15:05:18 +00:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'Escape':
|
|
|
|
|
cancel();
|
|
|
|
|
props.close();
|
|
|
|
|
break;
|
2020-01-31 22:38:40 +00:00
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
case 'ArrowUp':
|
|
|
|
|
if (select.value !== -1) {
|
|
|
|
|
cancel();
|
|
|
|
|
selectPrev();
|
2020-01-31 22:38:40 +00:00
|
|
|
|
} else {
|
2022-01-10 15:05:18 +00:00
|
|
|
|
props.close();
|
2018-02-25 09:40:39 +00:00
|
|
|
|
}
|
2022-01-10 15:05:18 +00:00
|
|
|
|
break;
|
2018-02-25 09:40:39 +00:00
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
case 'ArrowDown':
|
|
|
|
|
cancel();
|
|
|
|
|
selectNext();
|
|
|
|
|
break;
|
2018-02-20 11:29:52 +00:00
|
|
|
|
|
2023-12-18 05:59:55 +00:00
|
|
|
|
case 'Tab':
|
|
|
|
|
if (event.shiftKey) {
|
|
|
|
|
if (select.value !== -1) {
|
|
|
|
|
cancel();
|
|
|
|
|
selectPrev();
|
|
|
|
|
} else {
|
|
|
|
|
props.close();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
cancel();
|
|
|
|
|
selectNext();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
default:
|
2022-04-28 01:56:18 +00:00
|
|
|
|
event.stopPropagation();
|
2022-01-10 15:05:18 +00:00
|
|
|
|
props.textarea.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectNext() {
|
|
|
|
|
if (++select.value >= items.value.length) select.value = 0;
|
|
|
|
|
if (items.value.length === 0) select.value = -1;
|
|
|
|
|
applySelect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectPrev() {
|
|
|
|
|
if (--select.value < 0) select.value = items.value.length - 1;
|
|
|
|
|
applySelect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applySelect() {
|
|
|
|
|
for (const el of Array.from(items.value)) {
|
|
|
|
|
el.removeAttribute('data-selected');
|
|
|
|
|
}
|
2020-01-31 22:38:40 +00:00
|
|
|
|
|
2022-01-10 15:05:18 +00:00
|
|
|
|
if (select.value !== -1) {
|
|
|
|
|
items.value[select.value].setAttribute('data-selected', 'true');
|
|
|
|
|
(items.value[select.value] as any).focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function chooseUser() {
|
|
|
|
|
props.close();
|
|
|
|
|
os.selectUser().then(user => {
|
|
|
|
|
complete('user', user);
|
|
|
|
|
props.textarea.focus();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onUpdated(() => {
|
|
|
|
|
setPosition();
|
2022-03-01 12:36:20 +00:00
|
|
|
|
items.value = suggests.value?.children ?? [];
|
2022-01-10 15:05:18 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
setPosition();
|
|
|
|
|
|
|
|
|
|
props.textarea.addEventListener('keydown', onKeydown);
|
|
|
|
|
|
2023-07-17 05:10:40 +00:00
|
|
|
|
document.body.addEventListener('mousedown', onMousedown);
|
2022-01-10 15:05:18 +00:00
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
exec();
|
|
|
|
|
|
|
|
|
|
watch(() => props.q, () => {
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
exec();
|
2020-01-31 22:42:21 +00:00
|
|
|
|
});
|
2022-01-10 15:05:18 +00:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
props.textarea.removeEventListener('keydown', onKeydown);
|
2020-10-17 11:12:00 +00:00
|
|
|
|
|
2023-07-17 05:10:40 +00:00
|
|
|
|
document.body.removeEventListener('mousedown', onMousedown);
|
2018-02-20 11:29:52 +00:00
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
<style lang="scss" module>
|
|
|
|
|
.root {
|
2020-01-29 19:37:25 +00:00
|
|
|
|
position: fixed;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
margin-top: calc(1em + 8px);
|
2023-01-02 03:15:26 +00:00
|
|
|
|
overflow: clip;
|
2020-01-29 19:37:25 +00:00
|
|
|
|
transition: top 0.1s ease, left 0.1s ease;
|
2023-01-14 11:51:07 +00:00
|
|
|
|
}
|
2020-01-29 19:37:25 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
.list {
|
|
|
|
|
display: block;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
max-height: 190px;
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
list-style: none;
|
|
|
|
|
}
|
2020-01-29 19:37:25 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
.item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: clip;
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
cursor: default;
|
|
|
|
|
user-select: none;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: var(--X3);
|
2020-01-29 19:37:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
&[data-selected='true'] {
|
|
|
|
|
background: var(--accent);
|
|
|
|
|
color: #fff !important;
|
|
|
|
|
}
|
2020-01-29 19:37:25 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
&:active {
|
|
|
|
|
background: var(--accentDarken);
|
|
|
|
|
color: #fff !important;
|
2020-01-29 19:37:25 +00:00
|
|
|
|
}
|
2023-01-14 11:51:07 +00:00
|
|
|
|
}
|
2020-01-29 19:37:25 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
.avatar {
|
|
|
|
|
min-width: 28px;
|
|
|
|
|
min-height: 28px;
|
|
|
|
|
max-width: 28px;
|
|
|
|
|
max-height: 28px;
|
|
|
|
|
margin: 0 8px 0 0;
|
2023-10-31 18:44:34 +00:00
|
|
|
|
border-radius: var(--radius-full);
|
2023-01-14 11:51:07 +00:00
|
|
|
|
}
|
2022-12-30 03:00:50 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
.userName {
|
|
|
|
|
margin: 0 8px 0 0;
|
|
|
|
|
}
|
2020-01-29 19:37:25 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
.emoji {
|
2023-01-15 00:09:44 +00:00
|
|
|
|
flex-shrink: 0 !important;
|
|
|
|
|
display: flex !important;
|
|
|
|
|
margin: 0 4px 0 0 !important;
|
|
|
|
|
height: 24px !important;
|
|
|
|
|
width: 24px !important;
|
|
|
|
|
justify-content: center !important;
|
|
|
|
|
align-items: center !important;
|
|
|
|
|
font-size: 20px !important;
|
|
|
|
|
pointer-events: none !important;
|
2023-01-14 11:51:07 +00:00
|
|
|
|
}
|
2022-12-30 04:02:18 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
.emojiImg {
|
|
|
|
|
height: 24px;
|
|
|
|
|
width: 24px;
|
|
|
|
|
object-fit: scale-down;
|
|
|
|
|
}
|
2021-09-25 17:55:11 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
.emojiName {
|
|
|
|
|
flex-shrink: 1;
|
|
|
|
|
}
|
2021-09-25 17:55:11 +00:00
|
|
|
|
|
2023-01-14 11:51:07 +00:00
|
|
|
|
.emojiAlias {
|
|
|
|
|
flex-shrink: 9999999;
|
|
|
|
|
margin: 0 0 0 8px;
|
2020-01-29 19:37:25 +00:00
|
|
|
|
}
|
2018-02-20 11:29:52 +00:00
|
|
|
|
</style>
|