diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
| commit | 9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch) | |
| tree | ce5959571a981b9c4047da3c7b3fd080aa44222c /packages/client/src/components/MkAutocomplete.vue | |
| parent | wip: retention for dashboard (diff) | |
| download | misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2 misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip | |
rename: client -> frontend
Diffstat (limited to 'packages/client/src/components/MkAutocomplete.vue')
| -rw-r--r-- | packages/client/src/components/MkAutocomplete.vue | 476 |
1 files changed, 0 insertions, 476 deletions
diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue deleted file mode 100644 index 72783921d5..0000000000 --- a/packages/client/src/components/MkAutocomplete.vue +++ /dev/null @@ -1,476 +0,0 @@ -<template> -<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}"> - <ol v-if="type === 'user'" ref="suggests" class="users"> - <li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown"> - <img class="avatar" :src="user.avatarUrl"/> - <span class="name"> - <MkUserName :key="user.id" :user="user"/> - </span> - <span class="username">@{{ acct(user) }}</span> - </li> - <li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li> - </ol> - <ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags"> - <li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown"> - <span class="name">{{ hashtag }}</span> - </li> - </ol> - <ol v-else-if="emojis.length > 0" ref="suggests" class="emojis"> - <li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> - <span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span> - <span v-else-if="defaultStore.state.emojiStyle != 'native'" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span> - <span v-else class="emoji">{{ emoji.emoji }}</span> - <!-- eslint-disable-next-line vue/no-v-html --> - <span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> - <span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span> - </li> - </ol> - <ol v-else-if="mfmTags.length > 0" ref="suggests" class="mfmTags"> - <li v-for="tag in mfmTags" tabindex="-1" @click="complete(type, tag)" @keydown="onKeydown"> - <span class="tag">{{ tag }}</span> - </li> - </ol> -</div> -</template> - -<script lang="ts"> -import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; -import contains from '@/scripts/contains'; -import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; -import { acct } from '@/filters/user'; -import * as os from '@/os'; -import { MFM_TAGS } from '@/scripts/mfm-tags'; -import { defaultStore } from '@/store'; -import { emojilist } from '@/scripts/emojilist'; -import { instance } from '@/instance'; -import { i18n } from '@/i18n'; - -type EmojiDef = { - emoji: string; - name: string; - aliasOf?: string; - url?: string; - isCustomEmoji?: boolean; -}; - -const lib = emojilist.filter(x => x.category !== 'flags'); - -const emjdb: EmojiDef[] = lib.map(x => ({ - emoji: x.char, - name: x.name, - url: char2path(x.char), -})); - -const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; - -for (const x of lib) { - if (x.keywords) { - for (const k of x.keywords) { - emjdb.push({ - emoji: x.char, - name: k, - aliasOf: x.name, - url: char2path(x.char), - }); - } - } -} - -emjdb.sort((a, b) => a.name.length - b.name.length); - -//#region Construct Emoji DB -const customEmojis = instance.emojis; -const emojiDefinitions: EmojiDef[] = []; - -for (const x of customEmojis) { - emojiDefinitions.push({ - name: x.name, - emoji: `:${x.name}:`, - url: x.url, - isCustomEmoji: true, - }); - - if (x.aliases) { - for (const alias of x.aliases) { - emojiDefinitions.push({ - name: alias, - aliasOf: x.name, - emoji: `:${x.name}:`, - url: x.url, - isCustomEmoji: true, - }); - } - } -} - -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; - textarea: HTMLTextAreaElement; - close: () => void; - x: number; - y: number; -}>(); - -const emit = defineEmits<{ - (event: 'done', value: { type: string; value: any }): void; - (event: 'closed'): void; -}>(); - -const suggests = ref<Element>(); -const rootEl = ref<HTMLDivElement>(); - -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; - recents = recents.filter((emoji: any) => emoji !== value); - recents.unshift(value); - defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); - } -} - -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)'; - } -} - -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; - } - - const cacheKey = `autocomplete:user:${props.q}`; - const cache = sessionStorage.getItem(cacheKey); - - if (cache) { - users.value = JSON.parse(cache); - fetching.value = false; - } else { - os.api('users/search-by-username-and-host', { - username: props.q, - limit: 10, - detail: false, - }).then(searchedUsers => { - users.value = searchedUsers as any[]; - fetching.value = false; - // キャッシュ - sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers)); - }); - } - } else if (props.type === 'hashtag') { - if (!props.q || props.q === '') { - hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]'); - 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, - limit: 30, - }).then(searchedHashtags => { - hashtags.value = searchedHashtags as any[]; - fetching.value = false; - // キャッシュ - sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags)); - }); - } - } - } else if (props.type === 'emoji') { - if (!props.q || props.q === '') { - // 最近使った絵文字をサジェスト - emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; - return; - } - - 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; - }); - - 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; - }); - } - - 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; - }); - } - - emojis.value = matched; - } else if (props.type === 'mfmTag') { - if (!props.q || props.q === '') { - mfmTags.value = MFM_TAGS; - return; - } - - mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q ?? '')); - } -} - -function onMousedown(event: Event) { - if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); -} - -function onKeydown(event: KeyboardEvent) { - const cancel = () => { - event.preventDefault(); - event.stopPropagation(); - }; - - switch (event.key) { - case 'Enter': - if (select.value !== -1) { - cancel(); - (items.value[select.value] as any).click(); - } else { - props.close(); - } - break; - - case 'Escape': - cancel(); - props.close(); - break; - - case 'ArrowUp': - if (select.value !== -1) { - cancel(); - selectPrev(); - } else { - props.close(); - } - break; - - case 'Tab': - case 'ArrowDown': - cancel(); - selectNext(); - break; - - default: - event.stopPropagation(); - 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'); - } - - 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(); - items.value = suggests.value?.children ?? []; -}); - -onMounted(() => { - setPosition(); - - props.textarea.addEventListener('keydown', onKeydown); - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', onMousedown); - } - - nextTick(() => { - exec(); - - watch(() => props.q, () => { - nextTick(() => { - exec(); - }); - }); - }); -}); - -onBeforeUnmount(() => { - props.textarea.removeEventListener('keydown', onKeydown); - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', onMousedown); - } -}); -</script> - -<style lang="scss" scoped> -.swhvrteh { - position: fixed; - max-width: 100%; - margin-top: calc(1em + 8px); - overflow: hidden; - transition: top 0.1s ease, left 0.1s ease; - - > ol { - display: block; - margin: 0; - padding: 4px 0; - max-height: 190px; - max-width: 500px; - overflow: auto; - list-style: none; - - > li { - display: flex; - align-items: center; - padding: 4px 12px; - white-space: nowrap; - overflow: hidden; - font-size: 0.9em; - cursor: default; - - &, * { - user-select: none; - } - - * { - overflow: hidden; - text-overflow: ellipsis; - } - - &:hover { - background: var(--X3); - } - - &[data-selected='true'] { - background: var(--accent); - - &, * { - color: #fff !important; - } - } - - &:active { - background: var(--accentDarken); - - &, * { - color: #fff !important; - } - } - } - } - - > .users > li { - - .avatar { - min-width: 28px; - min-height: 28px; - max-width: 28px; - max-height: 28px; - margin: 0 8px 0 0; - border-radius: 100%; - } - - .name { - margin: 0 8px 0 0; - } - } - - > .emojis > li { - - .emoji { - display: inline-block; - margin: 0 4px 0 0; - width: 24px; - - > img { - width: 24px; - vertical-align: bottom; - } - } - - .alias { - margin: 0 0 0 8px; - } - } - - > .mfmTags > li { - - .name { - } - } -} -</style> |