From 9c9cd168ee0c8a9d2528ef2eda6f7de0b8d8da41 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Sat, 14 Nov 2020 11:47:30 +0900 Subject: [PATCH] Improve emoji picker --- src/client/components/emoji-picker.vue | 93 ++++++---- src/client/components/note.vue | 44 ++--- src/client/components/reaction-picker.vue | 214 ---------------------- src/client/pages/settings/reaction.vue | 17 +- 4 files changed, 75 insertions(+), 293 deletions(-) delete mode 100644 src/client/components/reaction-picker.vue diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue index 270951962f..5d60f2eb51 100644 --- a/src/client/components/emoji-picker.vue +++ b/src/client/components/emoji-picker.vue @@ -1,7 +1,7 @@ <template> <MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')"> - <div class="omfetrab _popup"> - <input ref="search" class="search" v-model.trim="q" :placeholder="$t('search')" @paste.stop="paste" @keyup.enter="done()" autofocus> + <div class="omfetrab _popup" :class="{ compact }"> + <input ref="search" class="search" :class="{ filled: q != null && q != '' }" v-model.trim="q" :placeholder="$t('search')" @paste.stop="paste" @keyup.enter="done()"> <div class="emojis"> <section class="result"> <div v-if="searchResultCustom.length > 0"> @@ -43,7 +43,7 @@ </section> <section> - <header class="_acrylic"><Fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header> + <header class="_acrylic"><Fa :icon="faClock" fixed-width/> {{ $t('recentUsed') }}</header> <div> <button v-for="emoji in $store.state.device.recentlyUsedEmojis" class="_button" @@ -94,7 +94,7 @@ import { defineComponent, markRaw } from 'vue'; import { emojilist } from '../../misc/emojilist'; import { getStaticImageUrl } from '@/scripts/get-static-image-url'; -import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser, faChevronDown } from '@fortawesome/free-solid-svg-icons'; +import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faClock, faUser, faChevronDown } from '@fortawesome/free-solid-svg-icons'; import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons'; import MkModal from '@/components/ui/modal.vue'; import Particle from '@/components/particle.vue'; @@ -112,6 +112,9 @@ export default defineComponent({ overridePinned: { required: false }, + compact: { + required: false + }, }, emits: ['done', 'closed'], @@ -127,7 +130,7 @@ export default defineComponent({ q: null, searchResultCustom: [], searchResultUnicode: [], - faGlobe, faHistory, faChevronDown, + faGlobe, faClock, faChevronDown, categories: [{ name: 'face', icon: faLaugh, @@ -311,9 +314,12 @@ export default defineComponent({ }, mounted() { - this.$refs.search.focus({ - preventScroll: true - }); + const isIos = navigator.userAgent.includes('WebKit') && !navigator.userAgent.includes('Chrome'); + if (!isIos) { + this.$refs.search.focus({ + preventScroll: true + }); + } }, methods: { @@ -379,8 +385,19 @@ export default defineComponent({ <style lang="scss" scoped> .omfetrab { - width: 350px; + $eachSize: 40px; + $pad: 8px; + + display: flex; + flex-direction: column; + width: ($eachSize * 7) + ($pad * 2); contain: content; + --height: 300px; + + &.compact { + width: ($eachSize * 5) + ($pad * 2); + --height: 210px; + } > .search { width: 100%; @@ -391,17 +408,27 @@ export default defineComponent({ border: none; background: transparent; color: var(--fg); + + &:not(.filled) { + order: 1; + z-index: 2; + box-shadow: 0px -1px 0 0px var(--divider); + } } > .emojis { - $height: 300px; - - height: $height; + height: var(--height); overflow-y: auto; overflow-x: hidden; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + > .index { - min-height: $height; + min-height: var(--height); position: relative; border-bottom: solid 1px var(--divider); @@ -428,45 +455,33 @@ export default defineComponent({ } > div { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - gap: 4px; - padding: 8px; + padding: $pad; > button { position: relative; padding: 0; - width: 100%; + width: $eachSize; + height: $eachSize; + border-radius: 4px; &:focus { outline: solid 2px var(--focus); z-index: 1; } - &:before { - content: ''; - display: block; - width: 1px; - height: 0; - padding-bottom: 100%; + &:hover { + background: rgba(0, 0, 0, 0.05); } - &:hover { - > * { - transform: scale(1.2); - transition: transform 0s; - } + &:active { + background: var(--accent); + box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); } > * { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: contain; - font-size: 28px; - transition: transform 0.2s ease; + font-size: 24px; + height: 1.25em; + vertical-align: -.25em; pointer-events: none; } } @@ -474,6 +489,10 @@ export default defineComponent({ &.result { border-bottom: solid 1px var(--divider); + + &:empty { + display: none; + } } &.unicode { diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 53972d9f6f..bf89cbf568 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -498,36 +498,20 @@ export default defineComponent({ react(viaKeyboard = false) { pleaseLogin(); this.blur(); - if (this.$store.state.device.useFullReactionPicker) { - os.popup(import('@/components/emoji-picker.vue'), { - src: this.$refs.reactButton, - }, { - done: reaction => { - if (reaction) { - os.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - } - this.focus(); - }, - }, 'closed'); - } else { - os.popup(import('@/components/reaction-picker.vue'), { - showFocus: viaKeyboard, - src: this.$refs.reactButton, - }, { - done: reaction => { - if (reaction) { - os.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - } - this.focus(); - }, - }, 'closed'); - } + os.popup(import('@/components/emoji-picker.vue'), { + src: this.$refs.reactButton, + compact: !this.$store.state.device.useFullReactionPicker + }, { + done: reaction => { + if (reaction) { + os.api('notes/reactions/create', { + noteId: this.appearNote.id, + reaction: reaction + }); + } + this.focus(); + }, + }, 'closed'); }, reactDirectly(reaction) { diff --git a/src/client/components/reaction-picker.vue b/src/client/components/reaction-picker.vue deleted file mode 100644 index fca1e858bb..0000000000 --- a/src/client/components/reaction-picker.vue +++ /dev/null @@ -1,214 +0,0 @@ -<template> -<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')"> - <div class="rdfaahpb _popup" v-hotkey="keymap"> - <div class="buttons" ref="buttons" :class="{ showFocus }"> - <button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction" v-particle><XReactionIcon :reaction="reaction"/></button> - </div> - <input class="text" ref="text" v-model.trim="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText"> - </div> -</MkModal> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { emojiRegex } from '../../misc/emoji-regex'; -import XReactionIcon from '@/components/reaction-icon.vue'; -import MkModal from '@/components/ui/modal.vue'; -import { Autocomplete } from '@/scripts/autocomplete'; - -export default defineComponent({ - components: { - XReactionIcon, - MkModal, - }, - - props: { - reactions: { - required: false - }, - - showFocus: { - type: Boolean, - required: false, - default: false - }, - - src: { - required: false - }, - }, - - emits: ['done', 'closed'], - - data() { - return { - rs: this.reactions || this.$store.state.settings.reactions, - text: null, - focus: null - }; - }, - - computed: { - keymap(): any { - return { - 'esc': this.close, - 'enter|space|plus': this.choose, - 'up|k': this.focusUp, - 'left|h|shift+tab': this.focusLeft, - 'right|l|tab': this.focusRight, - 'down|j': this.focusDown, - '1': () => this.react(this.rs[0]), - '2': () => this.react(this.rs[1]), - '3': () => this.react(this.rs[2]), - '4': () => this.react(this.rs[3]), - '5': () => this.react(this.rs[4]), - '6': () => this.react(this.rs[5]), - '7': () => this.react(this.rs[6]), - '8': () => this.react(this.rs[7]), - '9': () => this.react(this.rs[8]), - '0': () => this.react(this.rs[9]), - }; - }, - }, - - watch: { - focus(i) { - this.$refs.buttons.children[i].focus({ - preventScroll: true - }); - } - }, - - mounted() { - this.$nextTick(() => { - this.focus = 0; - }); - - // TODO: detach when unmount - new Autocomplete(this.$refs.text, this, { model: 'text' }); - }, - - methods: { - close() { - this.$emit('done'); - this.$refs.modal.close(); - }, - - react(reaction) { - this.$emit('done', reaction); - this.$refs.modal.close(); - }, - - reactText() { - if (!this.text) return; - this.react(this.text); - }, - - tryReactText() { - if (!this.text) return; - if (!this.text.match(emojiRegex)) return; - this.reactText(); - }, - - focusUp() { - this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5); - }, - - focusDown() { - this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5); - }, - - focusRight() { - this.focus = this.focus == 9 ? 0 : (this.focus + 1); - }, - - focusLeft() { - this.focus = this.focus == 0 ? 9 : (this.focus - 1); - }, - - choose() { - this.$refs.buttons.children[this.focus].click(); - }, - } -}); -</script> - -<style lang="scss" scoped> -.rdfaahpb { - > .buttons { - padding: 6px 6px 0 6px; - width: 212px; - box-sizing: border-box; - text-align: center; - - @media (max-width: 1025px) { - padding: 8px 8px 0 8px; - width: 256px; - } - - &.showFocus { - > button:focus { - position: relative; - z-index: 1; - - &:after { - content: ""; - pointer-events: none; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - border: 2px solid var(--focus); - border-radius: 4px; - } - } - } - - > button { - padding: 0; - width: 40px; - height: 40px; - font-size: 24px; - border-radius: 2px; - - @media (max-width: 1025px) { - width: 48px; - height: 48px; - font-size: 26px; - } - - > * { - height: 1em; - } - - &: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); - } - } - } - - > .text { - width: 208px; - padding: 8px; - margin: 0 0 6px 0; - box-sizing: border-box; - text-align: center; - font-size: 16px; - outline: none; - border: none; - background: transparent; - color: var(--fg); - - @media (max-width: 1025px) { - width: 256px; - margin: 4px 0 8px 0; - } - } -} -</style> diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue index 8033b7e11d..52b9f3260b 100644 --- a/src/client/pages/settings/reaction.vue +++ b/src/client/pages/settings/reaction.vue @@ -80,18 +80,11 @@ export default defineComponent({ }, preview(ev) { - if (this.$store.state.device.useFullReactionPicker) { - os.popup(import('@/components/emoji-picker.vue'), { - overridePinned: this.splited, - src: ev.currentTarget || ev.target, - }, {}, 'closed'); - } else { - os.popup(import('@/components/reaction-picker.vue'), { - reactions: this.splited, - showFocus: false, - src: ev.currentTarget || ev.target, - }, {}, 'closed'); - } + os.popup(import('@/components/emoji-picker.vue'), { + overridePinned: this.splited, + compact: !this.$store.state.device.useFullReactionPicker, + src: ev.currentTarget || ev.target, + }, {}, 'closed'); }, setDefault() {