Improve emoji picker

This commit is contained in:
syuilo 2020-11-14 11:47:30 +09:00
parent abb3d2a8d9
commit 9c9cd168ee
4 changed files with 75 additions and 293 deletions

View file

@ -1,7 +1,7 @@
<template> <template>
<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')"> <MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="omfetrab _popup"> <div class="omfetrab _popup" :class="{ compact }">
<input ref="search" class="search" v-model.trim="q" :placeholder="$t('search')" @paste.stop="paste" @keyup.enter="done()" autofocus> <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"> <div class="emojis">
<section class="result"> <section class="result">
<div v-if="searchResultCustom.length > 0"> <div v-if="searchResultCustom.length > 0">
@ -43,7 +43,7 @@
</section> </section>
<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> <div>
<button v-for="emoji in $store.state.device.recentlyUsedEmojis" <button v-for="emoji in $store.state.device.recentlyUsedEmojis"
class="_button" class="_button"
@ -94,7 +94,7 @@
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import { emojilist } from '../../misc/emojilist'; import { emojilist } from '../../misc/emojilist';
import { getStaticImageUrl } from '@/scripts/get-static-image-url'; 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 { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
import MkModal from '@/components/ui/modal.vue'; import MkModal from '@/components/ui/modal.vue';
import Particle from '@/components/particle.vue'; import Particle from '@/components/particle.vue';
@ -112,6 +112,9 @@ export default defineComponent({
overridePinned: { overridePinned: {
required: false required: false
}, },
compact: {
required: false
},
}, },
emits: ['done', 'closed'], emits: ['done', 'closed'],
@ -127,7 +130,7 @@ export default defineComponent({
q: null, q: null,
searchResultCustom: [], searchResultCustom: [],
searchResultUnicode: [], searchResultUnicode: [],
faGlobe, faHistory, faChevronDown, faGlobe, faClock, faChevronDown,
categories: [{ categories: [{
name: 'face', name: 'face',
icon: faLaugh, icon: faLaugh,
@ -311,9 +314,12 @@ export default defineComponent({
}, },
mounted() { mounted() {
const isIos = navigator.userAgent.includes('WebKit') && !navigator.userAgent.includes('Chrome');
if (!isIos) {
this.$refs.search.focus({ this.$refs.search.focus({
preventScroll: true preventScroll: true
}); });
}
}, },
methods: { methods: {
@ -379,8 +385,19 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.omfetrab { .omfetrab {
width: 350px; $eachSize: 40px;
$pad: 8px;
display: flex;
flex-direction: column;
width: ($eachSize * 7) + ($pad * 2);
contain: content; contain: content;
--height: 300px;
&.compact {
width: ($eachSize * 5) + ($pad * 2);
--height: 210px;
}
> .search { > .search {
width: 100%; width: 100%;
@ -391,17 +408,27 @@ export default defineComponent({
border: none; border: none;
background: transparent; background: transparent;
color: var(--fg); color: var(--fg);
&:not(.filled) {
order: 1;
z-index: 2;
box-shadow: 0px -1px 0 0px var(--divider);
}
} }
> .emojis { > .emojis {
$height: 300px; height: var(--height);
height: $height;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
> .index { > .index {
min-height: $height; min-height: var(--height);
position: relative; position: relative;
border-bottom: solid 1px var(--divider); border-bottom: solid 1px var(--divider);
@ -428,45 +455,33 @@ export default defineComponent({
} }
> div { > div {
display: grid; padding: $pad;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: 4px;
padding: 8px;
> button { > button {
position: relative; position: relative;
padding: 0; padding: 0;
width: 100%; width: $eachSize;
height: $eachSize;
border-radius: 4px;
&:focus { &:focus {
outline: solid 2px var(--focus); outline: solid 2px var(--focus);
z-index: 1; z-index: 1;
} }
&:before {
content: '';
display: block;
width: 1px;
height: 0;
padding-bottom: 100%;
}
&:hover { &:hover {
> * { background: rgba(0, 0, 0, 0.05);
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; font-size: 24px;
top: 0; height: 1.25em;
left: 0; vertical-align: -.25em;
width: 100%;
height: 100%;
object-fit: contain;
font-size: 28px;
transition: transform 0.2s ease;
pointer-events: none; pointer-events: none;
} }
} }
@ -474,6 +489,10 @@ export default defineComponent({
&.result { &.result {
border-bottom: solid 1px var(--divider); border-bottom: solid 1px var(--divider);
&:empty {
display: none;
}
} }
&.unicode { &.unicode {

View file

@ -498,9 +498,9 @@ export default defineComponent({
react(viaKeyboard = false) { react(viaKeyboard = false) {
pleaseLogin(); pleaseLogin();
this.blur(); this.blur();
if (this.$store.state.device.useFullReactionPicker) {
os.popup(import('@/components/emoji-picker.vue'), { os.popup(import('@/components/emoji-picker.vue'), {
src: this.$refs.reactButton, src: this.$refs.reactButton,
compact: !this.$store.state.device.useFullReactionPicker
}, { }, {
done: reaction => { done: reaction => {
if (reaction) { if (reaction) {
@ -512,22 +512,6 @@ export default defineComponent({
this.focus(); this.focus();
}, },
}, 'closed'); }, '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');
}
}, },
reactDirectly(reaction) { reactDirectly(reaction) {

View file

@ -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>

View file

@ -80,18 +80,11 @@ export default defineComponent({
}, },
preview(ev) { preview(ev) {
if (this.$store.state.device.useFullReactionPicker) {
os.popup(import('@/components/emoji-picker.vue'), { os.popup(import('@/components/emoji-picker.vue'), {
overridePinned: this.splited, overridePinned: this.splited,
compact: !this.$store.state.device.useFullReactionPicker,
src: ev.currentTarget || ev.target, src: ev.currentTarget || ev.target,
}, {}, 'closed'); }, {}, 'closed');
} else {
os.popup(import('@/components/reaction-picker.vue'), {
reactions: this.splited,
showFocus: false,
src: ev.currentTarget || ev.target,
}, {}, 'closed');
}
}, },
setDefault() { setDefault() {