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/MkEmojiPicker.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/MkEmojiPicker.vue')
| -rw-r--r-- | packages/client/src/components/MkEmojiPicker.vue | 569 |
1 files changed, 0 insertions, 569 deletions
diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue deleted file mode 100644 index 814f71168a..0000000000 --- a/packages/client/src/components/MkEmojiPicker.vue +++ /dev/null @@ -1,569 +0,0 @@ -<template> -<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> - <input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()"> - <div ref="emojis" class="emojis"> - <section class="result"> - <div v-if="searchResultCustom.length > 0" class="body"> - <button - v-for="emoji in searchResultCustom" - :key="emoji.id" - class="_button item" - :title="emoji.name" - tabindex="0" - @click="chosen(emoji, $event)" - > - <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>--> - <img class="emoji" :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> - </button> - </div> - <div v-if="searchResultUnicode.length > 0" class="body"> - <button - v-for="emoji in searchResultUnicode" - :key="emoji.name" - class="_button item" - :title="emoji.name" - tabindex="0" - @click="chosen(emoji, $event)" - > - <MkEmoji class="emoji" :emoji="emoji.char"/> - </button> - </div> - </section> - - <div v-if="tab === 'index'" class="group index"> - <section v-if="showPinned"> - <div class="body"> - <button - v-for="emoji in pinned" - :key="emoji" - class="_button item" - tabindex="0" - @click="chosen(emoji, $event)" - > - <MkEmoji class="emoji" :emoji="emoji" :normal="true"/> - </button> - </div> - </section> - - <section> - <header class="_acrylic"><i class="ti ti-clock ti-fw"></i> {{ i18n.ts.recentUsed }}</header> - <div class="body"> - <button - v-for="emoji in recentlyUsedEmojis" - :key="emoji" - class="_button item" - @click="chosen(emoji, $event)" - > - <MkEmoji class="emoji" :emoji="emoji" :normal="true"/> - </button> - </div> - </section> - </div> - <div v-once class="group"> - <header class="_acrylic">{{ i18n.ts.customEmojis }}</header> - <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection> - </div> - <div v-once class="group"> - <header class="_acrylic">{{ i18n.ts.emoji }}</header> - <XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection> - </div> - </div> - <div class="tabs"> - <button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ti ti-asterisk ti-fw"></i></button> - <button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ti ti-mood-happy ti-fw"></i></button> - <button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ti ti-leaf ti-fw"></i></button> - <button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ti ti-hash ti-fw"></i></button> - </div> -</div> -</template> - -<script lang="ts" setup> -import { ref, computed, watch, onMounted } from 'vue'; -import * as Misskey from 'misskey-js'; -import XSection from '@/components/MkEmojiPicker.section.vue'; -import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; -import Ripple from '@/components/MkRipple.vue'; -import * as os from '@/os'; -import { isTouchUsing } from '@/scripts/touch'; -import { deviceKind } from '@/scripts/device-kind'; -import { emojiCategories, instance } from '@/instance'; -import { i18n } from '@/i18n'; -import { defaultStore } from '@/store'; - -const props = withDefaults(defineProps<{ - showPinned?: boolean; - asReactionPicker?: boolean; - maxHeight?: number; - asDrawer?: boolean; -}>(), { - showPinned: true, -}); - -const emit = defineEmits<{ - (ev: 'chosen', v: string): void; -}>(); - -const search = ref<HTMLInputElement>(); -const emojis = ref<HTMLDivElement>(); - -const { - reactions: pinned, - reactionPickerSize, - reactionPickerWidth, - reactionPickerHeight, - disableShowingAnimatedImages, - recentlyUsedEmojis, -} = defaultStore.reactiveState; - -const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1); -const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); -const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); -const customEmojiCategories = emojiCategories; -const customEmojis = instance.emojis; -const q = ref<string>(''); -const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); -const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); -const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); - -watch(q, () => { - if (emojis.value) emojis.value.scrollTop = 0; - - if (q.value === '') { - searchResultCustom.value = []; - searchResultUnicode.value = []; - return; - } - - const newQ = q.value.replace(/:/g, '').toLowerCase(); - - const searchCustom = () => { - const max = 8; - const emojis = customEmojis; - const matches = new Set<Misskey.entities.CustomEmoji>(); - - const exactMatch = emojis.find(emoji => emoji.name === newQ); - if (exactMatch) matches.add(exactMatch); - - if (newQ.includes(' ')) { // AND検索 - const keywords = newQ.split(' '); - - // 名前にキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } else { - for (const emoji of emojis) { - if (emoji.name.startsWith(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.startsWith(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.name.includes(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.includes(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } - - return matches; - }; - - const searchUnicode = () => { - const max = 8; - const emojis = emojilist; - const matches = new Set<UnicodeEmojiDef>(); - - const exactMatch = emojis.find(emoji => emoji.name === newQ); - if (exactMatch) matches.add(exactMatch); - - if (newQ.includes(' ')) { // AND検索 - const keywords = newQ.split(' '); - - // 名前にキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } else { - for (const emoji of emojis) { - if (emoji.name.startsWith(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.name.includes(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.includes(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } - - return matches; - }; - - searchResultCustom.value = Array.from(searchCustom()); - searchResultUnicode.value = Array.from(searchUnicode()); -}); - -function focus() { - if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) { - search.value?.focus({ - preventScroll: true, - }); - } -} - -function reset() { - if (emojis.value) emojis.value.scrollTop = 0; - q.value = ''; -} - -function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string { - return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`; -} - -function chosen(emoji: any, ev?: MouseEvent) { - const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(Ripple, { x, y }, {}, 'end'); - } - - const key = getKey(emoji); - emit('chosen', key); - - // 最近使った絵文字更新 - if (!pinned.value.includes(key)) { - let recents = defaultStore.state.recentlyUsedEmojis; - recents = recents.filter((emoji: any) => emoji !== key); - recents.unshift(key); - defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); - } -} - -function input(): void { - // Using custom input event instead of v-model to respond immediately on - // Android, where composition happens on all languages - // (v-model does not update during composition) - q.value = search.value?.value.trim() ?? ''; -} - -function paste(event: ClipboardEvent): void { - const pasted = event.clipboardData?.getData('text') ?? ''; - if (done(pasted)) { - event.preventDefault(); - } -} - -function done(query?: string): boolean | void { - if (query == null) query = q.value; - if (query == null || typeof query !== 'string') return; - - const q2 = query.replace(/:/g, ''); - const exactMatchCustom = customEmojis.find(emoji => emoji.name === q2); - if (exactMatchCustom) { - chosen(exactMatchCustom); - return true; - } - const exactMatchUnicode = emojilist.find(emoji => emoji.char === q2 || emoji.name === q2); - if (exactMatchUnicode) { - chosen(exactMatchUnicode); - return true; - } - if (searchResultCustom.value.length > 0) { - chosen(searchResultCustom.value[0]); - return true; - } - if (searchResultUnicode.value.length > 0) { - chosen(searchResultUnicode.value[0]); - return true; - } -} - -onMounted(() => { - focus(); -}); - -defineExpose({ - focus, - reset, -}); -</script> - -<style lang="scss" scoped> -.omfetrab { - $pad: 8px; - - display: flex; - flex-direction: column; - - &.s1 { - --eachSize: 40px; - } - - &.s2 { - --eachSize: 45px; - } - - &.s3 { - --eachSize: 50px; - } - - &.w1 { - width: calc((var(--eachSize) * 5) + (#{$pad} * 2)); - --columns: 1fr 1fr 1fr 1fr 1fr; - } - - &.w2 { - width: calc((var(--eachSize) * 6) + (#{$pad} * 2)); - --columns: 1fr 1fr 1fr 1fr 1fr 1fr; - } - - &.w3 { - width: calc((var(--eachSize) * 7) + (#{$pad} * 2)); - --columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - &.w4 { - width: calc((var(--eachSize) * 8) + (#{$pad} * 2)); - --columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - &.w5 { - width: calc((var(--eachSize) * 9) + (#{$pad} * 2)); - --columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - &.h1 { - height: calc((var(--eachSize) * 4) + (#{$pad} * 2)); - } - - &.h2 { - height: calc((var(--eachSize) * 6) + (#{$pad} * 2)); - } - - &.h3 { - height: calc((var(--eachSize) * 8) + (#{$pad} * 2)); - } - - &.h4 { - height: calc((var(--eachSize) * 10) + (#{$pad} * 2)); - } - - &.asDrawer { - width: 100% !important; - - > .emojis { - ::v-deep(section) { - > header { - height: 32px; - line-height: 32px; - padding: 0 12px; - font-size: 15px; - } - - > .body { - display: grid; - grid-template-columns: var(--columns); - font-size: 30px; - - > .item { - aspect-ratio: 1 / 1; - width: auto; - height: auto; - min-width: 0; - } - } - } - } - } - - > .search { - width: 100%; - padding: 12px; - box-sizing: border-box; - font-size: 1em; - outline: none; - border: none; - background: transparent; - color: var(--fg); - - &:not(.filled) { - order: 1; - z-index: 2; - box-shadow: 0px -1px 0 0px var(--divider); - } - } - - > .tabs { - display: flex; - display: none; - - > .tab { - flex: 1; - height: 38px; - border-top: solid 0.5px var(--divider); - - &.active { - border-top: solid 1px var(--accent); - color: var(--accent); - } - } - } - - > .emojis { - height: 100%; - overflow-y: auto; - overflow-x: hidden; - - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - - > .group { - &:not(.index) { - padding: 4px 0 8px 0; - border-top: solid 0.5px var(--divider); - } - - > header { - /*position: sticky; - top: 0; - left: 0;*/ - height: 32px; - line-height: 32px; - z-index: 2; - padding: 0 8px; - font-size: 12px; - } - } - - ::v-deep(section) { - > header { - position: sticky; - top: 0; - left: 0; - height: 32px; - line-height: 32px; - z-index: 1; - padding: 0 8px; - font-size: 12px; - cursor: pointer; - - &:hover { - color: var(--accent); - } - } - - > .body { - position: relative; - padding: $pad; - - > .item { - position: relative; - padding: 0; - width: var(--eachSize); - height: var(--eachSize); - contain: strict; - border-radius: 4px; - font-size: 24px; - - &:focus-visible { - outline: solid 2px var(--focus); - z-index: 1; - } - - &:hover { - background: rgba(0, 0, 0, 0.05); - } - - &:active { - background: var(--accent); - box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); - } - - > .emoji { - height: 1.25em; - vertical-align: -.25em; - pointer-events: none; - } - } - } - - &.result { - border-bottom: solid 0.5px var(--divider); - - &:empty { - display: none; - } - } - } - } -} -</style> |