parent
0acacf7a8e
commit
0fbf56219f
7 changed files with 361 additions and 25 deletions
|
@ -379,6 +379,17 @@ common/views/components/poll-editor.vue:
|
|||
common/views/components/reaction-picker.vue:
|
||||
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:
|
||||
username: "ユーザー名"
|
||||
password: "パスワード"
|
||||
|
|
200
src/client/app/common/views/components/emoji-picker.vue
Normal file
200
src/client/app/common/views/components/emoji-picker.vue
Normal 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>
|
|
@ -22,7 +22,7 @@ export default Vue.extend({
|
|||
},
|
||||
customEmojis: {
|
||||
required: false,
|
||||
default: []
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -15,11 +15,16 @@
|
|||
<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a>
|
||||
</div>
|
||||
<input v-show="useCw" v-model="cw" :placeholder="$t('annotations')">
|
||||
<textarea :class="{ with: (files.length != 0 || poll) }"
|
||||
ref="text" v-model="text" :disabled="posting"
|
||||
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
|
||||
v-autocomplete="'text'"
|
||||
></textarea>
|
||||
<div class="textarea">
|
||||
<textarea :class="{ with: (files.length != 0 || poll) }"
|
||||
ref="text" v-model="text" :disabled="posting"
|
||||
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
|
||||
v-autocomplete="'text'"
|
||||
></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">
|
||||
<x-draggable :list="files" :options="{ animation: 150 }">
|
||||
<div v-for="file in files" :key="file.id">
|
||||
|
@ -377,6 +382,19 @@ export default Vue.extend({
|
|||
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() {
|
||||
this.posting = true;
|
||||
|
||||
|
@ -469,7 +487,7 @@ export default Vue.extend({
|
|||
|
||||
> .content
|
||||
> input
|
||||
> textarea
|
||||
> .textarea > textarea
|
||||
display block
|
||||
width 100%
|
||||
padding 12px
|
||||
|
@ -498,27 +516,48 @@ export default Vue.extend({
|
|||
> input
|
||||
margin-bottom 8px
|
||||
|
||||
> textarea
|
||||
margin 0
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
min-height 84px
|
||||
> .textarea
|
||||
> .emoji
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 10px
|
||||
font-size 18px
|
||||
color var(--text)
|
||||
opacity 0.5
|
||||
|
||||
&:hover
|
||||
& + *
|
||||
& + * + *
|
||||
border-color var(--primaryAlpha02)
|
||||
transition border-color .1s ease
|
||||
&:hover
|
||||
color var(--textHighlighted)
|
||||
opacity 1
|
||||
|
||||
&:focus
|
||||
& + *
|
||||
& + * + *
|
||||
border-color var(--primaryAlpha05)
|
||||
transition border-color 0s ease
|
||||
&:active
|
||||
color var(--primary)
|
||||
opacity 1
|
||||
|
||||
&.with
|
||||
border-bottom solid 1px var(--primaryAlpha01) !important
|
||||
border-radius 4px 4px 0 0
|
||||
> textarea
|
||||
margin 0
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
min-height 84px
|
||||
|
||||
&:hover
|
||||
& + *
|
||||
& + * + *
|
||||
border-color var(--primaryAlpha02)
|
||||
transition border-color .1s ease
|
||||
|
||||
&:focus
|
||||
& + *
|
||||
& + * + *
|
||||
border-color var(--primaryAlpha05)
|
||||
transition border-color 0s ease
|
||||
|
||||
& + .emoji
|
||||
opacity 0.7
|
||||
|
||||
&.with
|
||||
border-bottom solid 1px var(--primaryAlpha01) !important
|
||||
border-radius 4px 4px 0 0
|
||||
|
||||
> .visibleUsers
|
||||
margin-bottom 8px
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
secondary: '$secondary',
|
||||
bg: ':darken<8<$secondary',
|
||||
text: '$text',
|
||||
textHighlighted: ':lighten<7<$text',
|
||||
|
||||
scrollbarTrack: ':darken<5<$secondary',
|
||||
scrollbarHandle: ':lighten<5<$secondary',
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
secondary: '$secondary',
|
||||
bg: ':darken<8<$secondary',
|
||||
text: '$text',
|
||||
textHighlighted: ':darken<7<$text',
|
||||
|
||||
scrollbarTrack: '#fff',
|
||||
scrollbarHandle: '#00000033',
|
||||
|
|
Loading…
Reference in a new issue