summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkAutocomplete.vue
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/components/MkAutocomplete.vue')
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue113
1 files changed, 81 insertions, 32 deletions
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index a0cd066c06..6608eeaa47 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -15,12 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</li>
<li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
</ol>
- <ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list">
+ <ol v-else-if="type === 'hashtag' && hashtags.length > 0" ref="suggests" :class="$style.list">
<li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
<span class="name">{{ hashtag }}</span>
</li>
</ol>
- <ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list">
+ <ol v-else-if="type === 'emoji' || type === 'emojiComplete' && 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">
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/>
@@ -30,12 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="emoji.aliasOf" :class="$style.emojiAlias">({{ emoji.aliasOf }})</span>
</li>
</ol>
- <ol v-else-if="mfmTags.length > 0" ref="suggests" :class="$style.list">
+ <ol v-else-if="type === 'mfmTag' && 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>
</li>
</ol>
- <ol v-else-if="mfmParams.length > 0" ref="suggests" :class="$style.list">
+ <ol v-else-if="type === 'mfmParam' && mfmParams.length > 0" ref="suggests" :class="$style.list">
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
<span>{{ param }}</span>
</li>
@@ -44,26 +44,60 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
+import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import sanitizeHtml from 'sanitize-html';
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
-import contains from '@/scripts/contains.js';
import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@@/js/emoji-base.js';
+import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
+import type { EmojiDef } from '@/utility/search-emoji.js';
+import contains from '@/utility/contains.js';
import { acct } from '@/filters/user.js';
import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
-import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
-import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
+import { searchEmoji, searchEmojiExact } from '@/utility/search-emoji.js';
+import { prefer } from '@/preferences.js';
+
+export type CompleteInfo = {
+ user: {
+ payload: any;
+ query: string | null;
+ },
+ hashtag: {
+ payload: string;
+ query: string;
+ },
+ // `:emo` -> `:emoji:` or some unicode emoji
+ emoji: {
+ payload: string;
+ query: string;
+ },
+ // like emoji but for `:emoji:` -> unicode emoji
+ emojiComplete: {
+ payload: string;
+ query: string;
+ },
+ mfmTag: {
+ payload: string;
+ query: string;
+ },
+ mfmParam: {
+ payload: string;
+ query: {
+ tag: string;
+ params: string[];
+ };
+ },
+};
const lib = emojilist.filter(x => x.category !== 'flags');
-const emojiDb = computed(() => {
+const unicodeEmojiDB = computed(() => {
//#region Unicode Emoji
- const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : defaultStore.reactiveState.emojiStyle.value === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath;
+ const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : prefer.r.emojiStyle.value === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath;
const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
emoji: x.char,
@@ -71,7 +105,7 @@ const emojiDb = computed(() => {
url: char2path(x.char),
}));
- for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+ for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
for (const [emoji, keywords] of Object.entries(index)) {
for (const k of keywords) {
unicodeEmojiDB.push({
@@ -85,6 +119,12 @@ const emojiDb = computed(() => {
}
unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length);
+
+ return unicodeEmojiDB;
+});
+
+const emojiDb = computed(() => {
+ //#region Unicode Emoji
//#endregion
//#region Custom Emoji
@@ -112,7 +152,7 @@ const emojiDb = computed(() => {
customEmojiDB.sort((a, b) => a.name.length - b.name.length);
//#endregion
- return markRaw([...customEmojiDB, ...unicodeEmojiDB]);
+ return markRaw([...customEmojiDB, ...unicodeEmojiDB.value]);
});
export default {
@@ -121,23 +161,28 @@ export default {
};
</script>
-<script lang="ts" setup>
-const props = defineProps<{
- type: string;
- q: any;
- textarea: HTMLTextAreaElement;
+<script lang="ts" setup generic="T extends keyof CompleteInfo">
+type PropsType<T extends keyof CompleteInfo> = {
+ type: T;
+ q: CompleteInfo[T]['query'];
+ // なぜかわからないけど HTMLTextAreaElement | HTMLInputElement だと addEventListener/removeEventListenerがエラー
+ textarea: (HTMLTextAreaElement | HTMLInputElement) & HTMLElement;
close: () => void;
x: number;
y: number;
-}>();
+};
+//const props = defineProps<PropsType<keyof CompleteInfo>>();
+// ↑と同じだけど↓にしないとdiscriminated unionにならない。
+// https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions
+const props = defineProps<PropsType<'user'> | PropsType<'hashtag'> | PropsType<'emoji'> | PropsType<'emojiComplete'> | PropsType<'mfmTag'> | PropsType<'mfmParam'>>();
const emit = defineEmits<{
- (event: 'done', value: { type: string; value: any }): void;
+ <T extends keyof CompleteInfo>(event: 'done', value: { type: T; value: CompleteInfo[T]['payload'] }): void;
(event: 'closed'): void;
}>();
const suggests = ref<Element>();
-const rootEl = shallowRef<HTMLDivElement>();
+const rootEl = useTemplateRef('rootEl');
const fetching = ref(true);
const users = ref<any[]>([]);
@@ -149,14 +194,14 @@ const mfmParams = ref<string[]>([]);
const select = ref(-1);
const zIndex = os.claimZIndex('high');
-function complete(type: string, value: any) {
+function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T]['payload']) {
emit('done', { type, value });
emit('closed');
- if (type === 'emoji') {
- let recents = defaultStore.state.recentlyUsedEmojis;
+ if (type === 'emoji' || type === 'emojiComplete') {
+ let recents = store.s.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== value);
recents.unshift(value);
- defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+ store.set('recentlyUsedEmojis', recents.splice(0, 32));
}
}
@@ -197,8 +242,10 @@ function exec() {
users.value = JSON.parse(cache);
fetching.value = false;
} else {
+ const [username, host] = props.q.toString().split('@');
misskeyApi('users/search-by-username-and-host', {
- username: props.q,
+ username: username,
+ host: host,
limit: 10,
detail: false,
}).then(searchedUsers => {
@@ -234,11 +281,13 @@ function exec() {
} else if (props.type === 'emoji') {
if (!props.q || props.q === '') {
// 最近使った絵文字をサジェスト
- emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
+ emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
return;
}
emojis.value = searchEmoji(props.q.normalize('NFC').toLowerCase(), emojiDb.value);
+ } else if (props.type === 'emojiComplete') {
+ emojis.value = searchEmojiExact(props.q.normalize('NFC').toLowerCase(), unicodeEmojiDB.value);
} else if (props.type === 'mfmTag') {
if (!props.q || props.q === '') {
mfmTags.value = MFM_TAGS;
@@ -355,7 +404,7 @@ onMounted(() => {
props.textarea.addEventListener('keydown', onKeydown);
- document.body.addEventListener('mousedown', onMousedown);
+ window.document.body.addEventListener('mousedown', onMousedown);
nextTick(() => {
exec();
@@ -371,7 +420,7 @@ onMounted(() => {
onBeforeUnmount(() => {
props.textarea.removeEventListener('keydown', onKeydown);
- document.body.removeEventListener('mousedown', onMousedown);
+ window.document.body.removeEventListener('mousedown', onMousedown);
});
</script>
@@ -407,7 +456,7 @@ onBeforeUnmount(() => {
text-overflow: ellipsis;
&:hover {
- background: var(--MI_THEME-X3);
+ background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
}
&[data-selected='true'] {
@@ -416,7 +465,7 @@ onBeforeUnmount(() => {
}
&:active {
- background: var(--MI_THEME-accentDarken);
+ background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
color: #fff !important;
}
}