[Client] Emoji picker

Closes #3130
This commit is contained in:
syuilo 2018-11-13 00:12:55 +09:00
parent 0acacf7a8e
commit 0fbf56219f
No known key found for this signature in database
GPG key ID: BDC4C49D06AB9D69
7 changed files with 361 additions and 25 deletions

View file

@ -379,6 +379,17 @@ common/views/components/poll-editor.vue:
common/views/components/reaction-picker.vue: common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択" choose-reaction: "リアクションを選択"
common/views/components/emoji-picker.vue:
custom-emoji: "カスタム絵文字"
people: "人"
animals-and-nature: "動物&自然"
food-and-drink: "食べ物&飲み物"
activity: "アクティビティ"
travel-and-places: "場所"
objects: "物"
symbols: "記号"
flags: "旗"
common/views/components/signin.vue: common/views/components/signin.vue:
username: "ユーザー名" username: "ユーザー名"
password: "パスワード" password: "パスワード"

View file

@ -0,0 +1,200 @@
<template>
<div class="prlncendiewqqkrevzeruhndoakghvtx">
<header>
<button v-for="category in categories"
:title="category.text"
@click="go(category.ref)"
:class="{ active: category.isActive }"
>
<fa :icon="category.icon" fixed-width/>
</button>
</header>
<div class="emojis" ref="emojis" @scroll.passive="onScroll">
<section v-for="category in categories" :ref="category.ref">
<header><fa :icon="category.icon" fixed-width/> {{ category.text }}</header>
<div v-if="category.name">
<button v-for="emoji in Object.entries(lib).filter(([k, v]) => v.category === category.name)"
:title="emoji[0]"
@click="chosen(emoji[1].char)"
>
<mk-emoji :emoji="emoji[1].char"/>
</button>
</div>
<div v-else>
<button v-for="emoji in customEmojis"
:title="emoji.name"
@click="chosen(`:${emoji.name}:`)"
>
<img :src="emoji.url" :alt="emoji.name"/>
</button>
</div>
</section>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { lib } from 'emojilib';
export default Vue.extend({
i18n: i18n('common/views/components/emoji-picker.vue'),
data() {
return {
lib,
customEmojis: [],
categories: [{
ref: 'customEmojiSection',
text: this.$t('custom-emoji'),
icon: ['fas', 'asterisk'],
isActive: true
}, {
name: 'people',
ref: 'peopleSection',
text: this.$t('people'),
icon: ['far', 'laugh'],
isActive: false
}, {
name: 'animals_and_nature',
ref: 'animalsAndNatureSection',
text: this.$t('animals-and-nature'),
icon: ['fas', 'leaf'],
isActive: false
}, {
name: 'food_and_drink',
ref: 'foodAndDrinkSection',
text: this.$t('food-and-drink'),
icon: ['fas', 'utensils'],
isActive: false
}, {
name: 'activity',
ref: 'activitySection',
text: this.$t('activity'),
icon: ['fas', 'futbol'],
isActive: false
}, {
name: 'travel_and_places',
ref: 'travelAndPlacesSection',
text: this.$t('travel-and-places'),
icon: ['fas', 'city'],
isActive: false
}, {
name: 'objects',
ref: 'objectsSection',
text: this.$t('objects'),
icon: ['fas', 'poo-storm'],
isActive: false
}, {
name: 'symbols',
ref: 'symbolsSection',
text: this.$t('symbols'),
icon: ['far', 'heart'],
isActive: false
}, {
name: 'flags',
ref: 'flagsSection',
text: this.$t('flags'),
icon: ['far', 'flag'],
isActive: false
}]
}
},
created() {
this.customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
},
methods: {
go(ref) {
this.$refs.emojis.scrollTop = this.$refs[ref][0].offsetTop;
},
onScroll(e) {
const section = this.categories.forEach(x => {
const top = e.target.scrollTop;
const el = this.$refs[x.ref][0];
x.isActive = el.offsetTop <= top && el.offsetTop + el.offsetHeight > top;
});
},
chosen(emoji) {
this.$emit('chosen', emoji);
}
}
});
</script>
<style lang="stylus" scoped>
.prlncendiewqqkrevzeruhndoakghvtx
width 350px
background var(--face)
> header
display flex
> button
flex 1
padding 10px 0
font-size 16px
color var(--text)
transition color 0.2s ease
&:hover
color var(--textHighlighted)
transition color 0s
&.active
color var(--primary)
transition color 0s
> .emojis
height 300px
overflow-y auto
overflow-x hidden
> section
> header
position sticky
top 0
left 0
z-index 1
padding 8px
background var(--faceHeader)
color var(--text)
font-size 12px
> div
display grid
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
gap 4px
padding 8px
> button
padding 0
width 100%
&:before
content ''
display block
width 1px
height 0
padding-bottom 100%
&:hover
> *
transform scale(1.2)
transition transform 0s
> *
position absolute
top 0
left 0
width 100%
height 100%
font-size 28px
transition transform 0.2s ease
pointer-events none
</style>

View file

@ -22,7 +22,7 @@ export default Vue.extend({
}, },
customEmojis: { customEmojis: {
required: false, required: false,
default: [] default: () => []
} }
}, },

View file

@ -0,0 +1,84 @@
<template>
<div class="gcafiosrssbtbnbzqupfmglvzgiaipyv">
<x-picker @chosen="chosen"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import contains from '../../../common/scripts/contains';
export default Vue.extend({
components: {
XPicker: () => import('../../../common/views/components/emoji-picker.vue').then(m => m.default)
},
props: {
x: {
type: Number,
required: true
},
y: {
type: Number,
required: true
}
},
mounted() {
this.$nextTick(() => {
const width = this.$el.offsetWidth;
const height = this.$el.offsetHeight;
let x = this.x;
let y = this.y;
if (x + width - window.pageXOffset > window.innerWidth) {
x = window.innerWidth - width + window.pageXOffset;
}
if (y + height - window.pageYOffset > window.innerHeight) {
y = window.innerHeight - height + window.pageYOffset;
}
this.$el.style.left = x + 'px';
this.$el.style.top = y + 'px';
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.addEventListener('mousedown', this.onMousedown);
});
});
},
methods: {
onMousedown(e) {
e.preventDefault();
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
return false;
},
chosen(emoji) {
this.$emit('chosen', emoji);
this.close();
},
close() {
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.removeEventListener('mousedown', this.onMousedown);
});
this.$emit('closed');
this.destroyDom();
}
}
});
</script>
<style lang="stylus" scoped>
.gcafiosrssbtbnbzqupfmglvzgiaipyv
position fixed
top 0
left 0
z-index 3000
box-shadow 0 2px 12px 0 rgba(0, 0, 0, 0.3)
</style>

View file

@ -15,11 +15,16 @@
<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a> <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a>
</div> </div>
<input v-show="useCw" v-model="cw" :placeholder="$t('annotations')"> <input v-show="useCw" v-model="cw" :placeholder="$t('annotations')">
<div class="textarea">
<textarea :class="{ with: (files.length != 0 || poll) }" <textarea :class="{ with: (files.length != 0 || poll) }"
ref="text" v-model="text" :disabled="posting" ref="text" v-model="text" :disabled="posting"
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
v-autocomplete="'text'" v-autocomplete="'text'"
></textarea> ></textarea>
<button class="emoji" @click="emoji" ref="emoji">
<fa :icon="['far', 'laugh']"/>
</button>
</div>
<div class="files" :class="{ with: poll }" v-show="files.length != 0"> <div class="files" :class="{ with: poll }" v-show="files.length != 0">
<x-draggable :list="files" :options="{ animation: 150 }"> <x-draggable :list="files" :options="{ animation: 150 }">
<div v-for="file in files" :key="file.id"> <div v-for="file in files" :key="file.id">
@ -377,6 +382,19 @@ export default Vue.extend({
this.visibleUsers = erase(user, this.visibleUsers); this.visibleUsers = erase(user, this.visibleUsers);
}, },
async emoji() {
const Picker = await import('./emoji-picker-dialog.vue').then(m => m.default);
const button = this.$refs.emoji;
const rect = button.getBoundingClientRect();
const vm = this.$root.new(Picker, {
x: button.offsetWidth + rect.left + window.pageXOffset,
y: rect.top + window.pageYOffset
});
vm.$once('chosen', emoji => {
insertTextAtCursor(this.$refs.text, emoji);
});
},
post() { post() {
this.posting = true; this.posting = true;
@ -469,7 +487,7 @@ export default Vue.extend({
> .content > .content
> input > input
> textarea > .textarea > textarea
display block display block
width 100% width 100%
padding 12px padding 12px
@ -498,6 +516,24 @@ export default Vue.extend({
> input > input
margin-bottom 8px margin-bottom 8px
> .textarea
> .emoji
position absolute
top 0
right 0
padding 10px
font-size 18px
color var(--text)
opacity 0.5
&:hover
color var(--textHighlighted)
opacity 1
&:active
color var(--primary)
opacity 1
> textarea > textarea
margin 0 margin 0
max-width 100% max-width 100%
@ -516,6 +552,9 @@ export default Vue.extend({
border-color var(--primaryAlpha05) border-color var(--primaryAlpha05)
transition border-color 0s ease transition border-color 0s ease
& + .emoji
opacity 0.7
&.with &.with
border-bottom solid 1px var(--primaryAlpha01) !important border-bottom solid 1px var(--primaryAlpha01) !important
border-radius 4px 4px 0 0 border-radius 4px 4px 0 0

View file

@ -18,6 +18,7 @@
secondary: '$secondary', secondary: '$secondary',
bg: ':darken<8<$secondary', bg: ':darken<8<$secondary',
text: '$text', text: '$text',
textHighlighted: ':lighten<7<$text',
scrollbarTrack: ':darken<5<$secondary', scrollbarTrack: ':darken<5<$secondary',
scrollbarHandle: ':lighten<5<$secondary', scrollbarHandle: ':lighten<5<$secondary',

View file

@ -18,6 +18,7 @@
secondary: '$secondary', secondary: '$secondary',
bg: ':darken<8<$secondary', bg: ':darken<8<$secondary',
text: '$text', text: '$text',
textHighlighted: ':darken<7<$text',
scrollbarTrack: '#fff', scrollbarTrack: '#fff',
scrollbarHandle: '#00000033', scrollbarHandle: '#00000033',