parent
f2e071baaa
commit
f29d417b30
13 changed files with 563 additions and 113 deletions
191
src/client/components/emoji-picker-dialog.vue
Normal file
191
src/client/components/emoji-picker-dialog.vue
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
<template>
|
||||||
|
<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||||
|
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
|
||||||
|
</MkModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, markRaw } from 'vue';
|
||||||
|
import MkModal from '@/components/ui/modal.vue';
|
||||||
|
import MkEmojiPicker from '@/components/emoji-picker.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
MkModal,
|
||||||
|
MkEmojiPicker,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
showPinned: {
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
asReactionPicker: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['done', 'closed'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
chosen(emoji: any) {
|
||||||
|
this.$emit('done', emoji);
|
||||||
|
this.$refs.modal.close();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.omfetrab {
|
||||||
|
$pad: 8px;
|
||||||
|
--eachSize: 40px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
|
&.big {
|
||||||
|
--eachSize: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w1 {
|
||||||
|
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w2 {
|
||||||
|
width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w3 {
|
||||||
|
width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.h1 {
|
||||||
|
--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.h2 {
|
||||||
|
--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.h3 {
|
||||||
|
--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
> .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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .emojis {
|
||||||
|
height: var(--height);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .index {
|
||||||
|
min-height: var(--height);
|
||||||
|
position: relative;
|
||||||
|
border-bottom: solid 1px var(--divider);
|
||||||
|
|
||||||
|
> .arrow {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
> header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding: $pad;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
width: var(--eachSize);
|
||||||
|
height: var(--eachSize);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
font-size: 24px;
|
||||||
|
height: 1.25em;
|
||||||
|
vertical-align: -.25em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.result {
|
||||||
|
border-bottom: solid 1px var(--divider);
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unicode {
|
||||||
|
min-height: 384px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.custom {
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
197
src/client/components/emoji-picker-window.vue
Normal file
197
src/client/components/emoji-picker-window.vue
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
<template>
|
||||||
|
<MkWindow ref="window"
|
||||||
|
:initial-width="null"
|
||||||
|
:initial-height="null"
|
||||||
|
:can-resize="false"
|
||||||
|
:mini="true"
|
||||||
|
:front="true"
|
||||||
|
@closed="$emit('closed')"
|
||||||
|
>
|
||||||
|
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
|
||||||
|
</MkWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, markRaw } from 'vue';
|
||||||
|
import MkWindow from '@/components/ui/window.vue';
|
||||||
|
import MkEmojiPicker from '@/components/emoji-picker.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
MkWindow,
|
||||||
|
MkEmojiPicker,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
showPinned: {
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
asReactionPicker: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['chosen', 'closed'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
chosen(emoji: any) {
|
||||||
|
this.$emit('chosen', emoji);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.omfetrab {
|
||||||
|
$pad: 8px;
|
||||||
|
--eachSize: 40px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
|
&.big {
|
||||||
|
--eachSize: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w1 {
|
||||||
|
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w2 {
|
||||||
|
width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.w3 {
|
||||||
|
width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.h1 {
|
||||||
|
--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.h2 {
|
||||||
|
--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.h3 {
|
||||||
|
--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
> .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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .emojis {
|
||||||
|
height: var(--height);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .index {
|
||||||
|
min-height: var(--height);
|
||||||
|
position: relative;
|
||||||
|
border-bottom: solid 1px var(--divider);
|
||||||
|
|
||||||
|
> .arrow {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
> header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding: $pad;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
width: var(--eachSize);
|
||||||
|
height: var(--eachSize);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
font-size: 24px;
|
||||||
|
height: 1.25em;
|
||||||
|
vertical-align: -.25em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.result {
|
||||||
|
border-bottom: solid 1px var(--divider);
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unicode {
|
||||||
|
min-height: 384px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.custom {
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,93 +1,91 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
|
<div class="omfetrab _popup" :class="['w' + width, 'h' + height, { big }]">
|
||||||
<div class="omfetrab _popup" :class="['w' + width, 'h' + height, { big }]">
|
<input ref="search" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" v-model.trim="q" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
|
||||||
<input ref="search" class="search" :class="{ filled: q != null && q != '' }" v-model.trim="q" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
|
<div class="emojis" ref="emojis">
|
||||||
<div class="emojis" ref="emojis">
|
<section class="result">
|
||||||
<section class="result">
|
<div v-if="searchResultCustom.length > 0">
|
||||||
<div v-if="searchResultCustom.length > 0">
|
<button v-for="emoji in searchResultCustom"
|
||||||
<button v-for="emoji in searchResultCustom"
|
class="_button"
|
||||||
|
:title="emoji.name"
|
||||||
|
@click="chosen(emoji, $event)"
|
||||||
|
:key="emoji"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
|
||||||
|
<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="searchResultUnicode.length > 0">
|
||||||
|
<button v-for="emoji in searchResultUnicode"
|
||||||
|
class="_button"
|
||||||
|
:title="emoji.name"
|
||||||
|
@click="chosen(emoji, $event)"
|
||||||
|
:key="emoji.name"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<MkEmoji :emoji="emoji.char"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="index">
|
||||||
|
<section v-if="showPinned">
|
||||||
|
<div>
|
||||||
|
<button v-for="emoji in pinned"
|
||||||
|
class="_button"
|
||||||
|
@click="chosen(emoji, $event)"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<MkEmoji :emoji="emoji" :normal="true"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<header class="_acrylic"><Fa :icon="faClock" fixed-width/> {{ $ts.recentUsed }}</header>
|
||||||
|
<div>
|
||||||
|
<button v-for="emoji in $store.state.recentlyUsedEmojis"
|
||||||
class="_button"
|
class="_button"
|
||||||
:title="emoji.name"
|
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
:key="emoji"
|
:key="emoji"
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
|
<MkEmoji :emoji="emoji" :normal="true"/>
|
||||||
<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="searchResultUnicode.length > 0">
|
|
||||||
<button v-for="emoji in searchResultUnicode"
|
|
||||||
class="_button"
|
|
||||||
:title="emoji.name"
|
|
||||||
@click="chosen(emoji, $event)"
|
|
||||||
:key="emoji.name"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<MkEmoji :emoji="emoji.char"/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="index">
|
<div class="arrow"><Fa :icon="faChevronDown"/></div>
|
||||||
<section v-if="showPinned">
|
|
||||||
<div>
|
|
||||||
<button v-for="emoji in pinned"
|
|
||||||
class="_button"
|
|
||||||
@click="chosen(emoji, $event)"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<MkEmoji :emoji="emoji" :normal="true"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<header class="_acrylic"><Fa :icon="faClock" fixed-width/> {{ $ts.recentUsed }}</header>
|
|
||||||
<div>
|
|
||||||
<button v-for="emoji in $store.state.recentlyUsedEmojis"
|
|
||||||
class="_button"
|
|
||||||
@click="chosen(emoji, $event)"
|
|
||||||
:key="emoji"
|
|
||||||
>
|
|
||||||
<MkEmoji :emoji="emoji" :normal="true"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="arrow"><Fa :icon="faChevronDown"/></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section v-for="category in customEmojiCategories" :key="'custom:' + category" class="custom">
|
|
||||||
<header class="_acrylic" v-appear="() => visibleCategories[category] = true">{{ category || $ts.other }}</header>
|
|
||||||
<div v-if="visibleCategories[category]">
|
|
||||||
<button v-for="emoji in customEmojis.filter(e => e.category === category)"
|
|
||||||
class="_button"
|
|
||||||
:title="emoji.name"
|
|
||||||
@click="chosen(emoji, $event)"
|
|
||||||
:key="emoji.name"
|
|
||||||
>
|
|
||||||
<img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-for="category in categories" :key="category.name" class="unicode">
|
|
||||||
<header class="_acrylic" v-appear="() => category.isActive = true"><Fa :icon="category.icon" fixed-width/> {{ category.name }}</header>
|
|
||||||
<div v-if="category.isActive">
|
|
||||||
<button v-for="emoji in emojilist.filter(e => e.category === category.name)"
|
|
||||||
class="_button"
|
|
||||||
:title="emoji.name"
|
|
||||||
@click="chosen(emoji, $event)"
|
|
||||||
:key="emoji.name"
|
|
||||||
>
|
|
||||||
<MkEmoji :emoji="emoji.char"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section v-for="category in customEmojiCategories" :key="'custom:' + category" class="custom">
|
||||||
|
<header class="_acrylic" v-appear="() => visibleCategories[category] = true">{{ category || $ts.other }}</header>
|
||||||
|
<div v-if="visibleCategories[category]">
|
||||||
|
<button v-for="emoji in customEmojis.filter(e => e.category === category)"
|
||||||
|
class="_button"
|
||||||
|
:title="emoji.name"
|
||||||
|
@click="chosen(emoji, $event)"
|
||||||
|
:key="emoji.name"
|
||||||
|
>
|
||||||
|
<img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-for="category in categories" :key="category.name" class="unicode">
|
||||||
|
<header class="_acrylic" v-appear="() => category.isActive = true"><Fa :icon="category.icon" fixed-width/> {{ category.name }}</header>
|
||||||
|
<div v-if="category.isActive">
|
||||||
|
<button v-for="emoji in emojilist.filter(e => e.category === category.name)"
|
||||||
|
class="_button"
|
||||||
|
:title="emoji.name"
|
||||||
|
@click="chosen(emoji, $event)"
|
||||||
|
:key="emoji.name"
|
||||||
|
>
|
||||||
|
<MkEmoji :emoji="emoji.char"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</MkModal>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -96,7 +94,6 @@ 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, faClock, 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 Particle from '@/components/particle.vue';
|
import Particle from '@/components/particle.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
||||||
|
@ -104,14 +101,7 @@ import { isMobile } from '@/scripts/is-mobile';
|
||||||
import { emojiCategories } from '@/instance';
|
import { emojiCategories } from '@/instance';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
|
||||||
MkModal,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
src: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
showPinned: {
|
showPinned: {
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
|
@ -121,7 +111,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['done', 'closed'],
|
emits: ['chosen'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -345,8 +335,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = this.getKey(emoji);
|
const key = this.getKey(emoji);
|
||||||
this.$emit('done', key);
|
this.$emit('chosen', key);
|
||||||
this.$refs.modal.close();
|
|
||||||
|
|
||||||
// 最近使った絵文字更新
|
// 最近使った絵文字更新
|
||||||
if (!this.pinned.includes(key)) {
|
if (!this.pinned.includes(key)) {
|
||||||
|
|
|
@ -523,7 +523,7 @@ export default defineComponent({
|
||||||
react(viaKeyboard = false) {
|
react(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
this.blur();
|
this.blur();
|
||||||
os.popup(import('@/components/emoji-picker.vue'), {
|
os.popup(import('@/components/emoji-picker-dialog.vue'), {
|
||||||
src: this.$refs.reactButton,
|
src: this.$refs.reactButton,
|
||||||
asReactionPicker: true
|
asReactionPicker: true
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -498,7 +498,7 @@ export default defineComponent({
|
||||||
react(viaKeyboard = false) {
|
react(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
this.blur();
|
this.blur();
|
||||||
os.popup(import('@/components/emoji-picker.vue'), {
|
os.popup(import('@/components/emoji-picker-dialog.vue'), {
|
||||||
src: this.$refs.reactButton,
|
src: this.$refs.reactButton,
|
||||||
asReactionPicker: true
|
asReactionPicker: true
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -606,9 +606,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
async insertEmoji(ev) {
|
async insertEmoji(ev) {
|
||||||
os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
|
os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
|
||||||
insertTextAtCursor(this.$refs.text, emoji);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showActions(ev) {
|
showActions(ev) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
|
<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
|
||||||
<div class="ebkgocck" v-if="showing">
|
<div class="ebkgocck" :class="{ front }" v-if="showing">
|
||||||
<div class="body _popup _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
<div class="body _popup _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||||
<div class="header" @contextmenu.prevent.stop="onContextmenu">
|
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
|
||||||
<slot v-if="closeRight" name="buttons"><button class="_button" style="pointer-events: none;"></button></slot>
|
<slot v-if="closeRight" name="buttons"><button class="_button" style="pointer-events: none;"></button></slot>
|
||||||
<button v-else class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
<button v-else class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
||||||
|
|
||||||
|
@ -92,6 +92,16 @@ export default defineComponent({
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
mini: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
front: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
contextmenu: {
|
contextmenu: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -387,6 +397,10 @@ export default defineComponent({
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 5000;
|
z-index: 5000;
|
||||||
|
|
||||||
|
&.front {
|
||||||
|
z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない
|
||||||
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
overflow: hidden; // overflow: clip; をSafariが対応したら消す
|
overflow: hidden; // overflow: clip; をSafariが対応したら消す
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
@ -397,17 +411,22 @@ export default defineComponent({
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
$height: 50px;
|
--height: 50px;
|
||||||
|
|
||||||
|
&.mini {
|
||||||
|
--height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
height: $height;
|
height: var(--height);
|
||||||
|
|
||||||
> ::v-deep(button) {
|
> ::v-deep(button) {
|
||||||
height: $height;
|
height: var(--height);
|
||||||
width: $height;
|
width: var(--height);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--fgHighlighted);
|
color: var(--fgHighlighted);
|
||||||
|
@ -417,7 +436,7 @@ export default defineComponent({
|
||||||
> .title {
|
> .title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: $height;
|
line-height: var(--height);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden; // overflow: clip; をSafariが対応したら消す
|
overflow: hidden; // overflow: clip; をSafariが対応したら消す
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
||||||
|
|
||||||
import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
|
import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import Stream from '@/scripts/stream';
|
import Stream from '@/scripts/stream';
|
||||||
import { apiUrl, debug } from '@/config';
|
import { apiUrl, debug } from '@/config';
|
||||||
|
@ -289,7 +292,7 @@ export async function selectDriveFolder(multiple: boolean) {
|
||||||
|
|
||||||
export async function pickEmoji(src?: HTMLElement, opts) {
|
export async function pickEmoji(src?: HTMLElement, opts) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
popup(import('@/components/emoji-picker.vue'), {
|
popup(import('@/components/emoji-picker-dialog.vue'), {
|
||||||
src,
|
src,
|
||||||
...opts
|
...opts
|
||||||
}, {
|
}, {
|
||||||
|
@ -300,6 +303,63 @@ export async function pickEmoji(src?: HTMLElement, opts) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AwaitType<T> =
|
||||||
|
T extends Promise<infer U> ? U :
|
||||||
|
T extends (...args: Array<any>) => Promise<infer V> ? V :
|
||||||
|
T;
|
||||||
|
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
|
||||||
|
let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
|
||||||
|
export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
|
||||||
|
if (openingEmojiPicker) return;
|
||||||
|
|
||||||
|
activeTextarea = initialTextarea;
|
||||||
|
|
||||||
|
const textareas = document.querySelectorAll('textarea, input');
|
||||||
|
for (const textarea of Array.from(textareas)) {
|
||||||
|
textarea.addEventListener('focus', () => {
|
||||||
|
activeTextarea = textarea;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(records => {
|
||||||
|
for (const record of records) {
|
||||||
|
for (const node of Array.from(record.addedNodes)) {
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
const textareas = node.querySelectorAll('textarea, input');
|
||||||
|
for (const textarea of Array.from(textareas)) {
|
||||||
|
if (textarea.dataset.preventEmojiInsert != null) return;
|
||||||
|
if (document.activeElement === textarea) activeTextarea = textarea;
|
||||||
|
textarea.addEventListener('focus', () => {
|
||||||
|
activeTextarea = textarea;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: false,
|
||||||
|
characterData: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
openingEmojiPicker = await popup(import('@/components/emoji-picker-window.vue'), {
|
||||||
|
src,
|
||||||
|
...opts
|
||||||
|
}, {
|
||||||
|
chosen: emoji => {
|
||||||
|
insertTextAtCursor(activeTextarea, emoji);
|
||||||
|
},
|
||||||
|
closed: () => {
|
||||||
|
openingEmojiPicker!.dispose();
|
||||||
|
openingEmojiPicker = null;
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function modalMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
|
export function modalMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let dispose;
|
let dispose;
|
||||||
|
|
|
@ -223,9 +223,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
async insertEmoji(ev) {
|
async insertEmoji(ev) {
|
||||||
os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
|
os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
|
||||||
insertTextAtCursor(this.$refs.text, emoji);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
preview(ev) {
|
preview(ev) {
|
||||||
os.popup(import('@/components/emoji-picker.vue'), {
|
os.popup(import('@/components/emoji-picker-dialog.vue'), {
|
||||||
asReactionPicker: true,
|
asReactionPicker: true,
|
||||||
src: ev.currentTarget || ev.target,
|
src: ev.currentTarget || ev.target,
|
||||||
}, {}, 'closed');
|
}, {}, 'closed');
|
||||||
|
|
|
@ -391,8 +391,8 @@ hr {
|
||||||
|
|
||||||
._acrylic {
|
._acrylic {
|
||||||
background: var(--acrylicPanel);
|
background: var(--acrylicPanel);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(15px);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
._vMargin {
|
._vMargin {
|
||||||
|
|
|
@ -504,7 +504,7 @@ export default defineComponent({
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
this.operating = true;
|
this.operating = true;
|
||||||
this.blur();
|
this.blur();
|
||||||
const { dispose } = await os.popup(import('@/components/emoji-picker.vue'), {
|
const { dispose } = await os.popup(import('@/components/emoji-picker-dialog.vue'), {
|
||||||
src: this.$refs.reactButton,
|
src: this.$refs.reactButton,
|
||||||
asReactionPicker: true
|
asReactionPicker: true
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -593,9 +593,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
async insertEmoji(ev) {
|
async insertEmoji(ev) {
|
||||||
os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
|
os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
|
||||||
insertTextAtCursor(this.$refs.text, emoji);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showActions(ev) {
|
showActions(ev) {
|
||||||
|
|
Loading…
Reference in a new issue