237 lines
6.7 KiB
Vue
237 lines
6.7 KiB
Vue
<!-- eslint-disable vue/multi-word-component-names -->
|
|
<script>
|
|
import {
|
|
GlButton,
|
|
GlIcon,
|
|
GlDisclosureDropdown,
|
|
GlSearchBoxByType,
|
|
GlTooltipDirective,
|
|
} from '@gitlab/ui';
|
|
import { findLastIndex } from 'lodash';
|
|
import VirtualList from 'vue-virtual-scroll-list';
|
|
import { getEmojiCategoryMap, state } from '~/emoji';
|
|
import { __ } from '~/locale';
|
|
import { CATEGORY_NAMES, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
|
|
import Category from './category.vue';
|
|
import EmojiList from './emoji_list.vue';
|
|
import { addToFrequentlyUsed, getEmojiCategories, hasFrequentlyUsedEmojis } from './utils';
|
|
|
|
export default {
|
|
components: {
|
|
GlButton,
|
|
GlIcon,
|
|
GlDisclosureDropdown,
|
|
GlSearchBoxByType,
|
|
VirtualList,
|
|
Category,
|
|
EmojiList,
|
|
},
|
|
directives: {
|
|
GlTooltip: GlTooltipDirective,
|
|
},
|
|
inject: {
|
|
newCustomEmojiPath: {
|
|
default: '',
|
|
},
|
|
},
|
|
props: {
|
|
toggleClass: {
|
|
type: [Array, String, Object],
|
|
required: false,
|
|
default: () => [],
|
|
},
|
|
dropdownClass: {
|
|
type: [Array, String, Object],
|
|
required: false,
|
|
default: () => [],
|
|
},
|
|
right: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isVisible: false,
|
|
currentCategory: 0,
|
|
searchValue: '',
|
|
};
|
|
},
|
|
computed: {
|
|
categoryNames() {
|
|
return CATEGORY_NAMES.filter((c) => {
|
|
if (c === FREQUENTLY_USED_KEY) return hasFrequentlyUsedEmojis();
|
|
if (c === 'custom') return !state.loading && getEmojiCategoryMap().custom.length > 0;
|
|
return true;
|
|
}).map((category) => ({
|
|
name: category,
|
|
icon: CATEGORY_ICON_MAP[category],
|
|
}));
|
|
},
|
|
placement() {
|
|
return this.right ? 'bottom-end' : 'bottom-start';
|
|
},
|
|
newCustomEmoji() {
|
|
return {
|
|
text: __('Create new emoji'),
|
|
href: this.newCustomEmojiPath,
|
|
extraAttrs: {
|
|
'data-testid': 'create-new-emoji',
|
|
},
|
|
};
|
|
},
|
|
},
|
|
methods: {
|
|
categoryAppeared(category) {
|
|
this.currentCategory = category;
|
|
},
|
|
async scrollToCategory(categoryName) {
|
|
const categories = await getEmojiCategories();
|
|
const { top } = categories[categoryName];
|
|
|
|
this.$refs.virtualScoller.setScrollTop(top);
|
|
},
|
|
selectEmoji({ category, emoji }) {
|
|
this.$emit('click', emoji);
|
|
this.$refs.dropdown.close();
|
|
|
|
if (category !== 'custom') {
|
|
addToFrequentlyUsed(emoji);
|
|
}
|
|
},
|
|
onSearchInput() {
|
|
this.$refs.virtualScoller.setScrollTop(0);
|
|
this.$refs.virtualScoller.forceRender();
|
|
},
|
|
async onScroll(event, { offset }) {
|
|
const categories = await getEmojiCategories();
|
|
|
|
this.currentCategory = findLastIndex(Object.values(categories), ({ top }) => offset >= top);
|
|
},
|
|
onShow() {
|
|
this.isVisible = true;
|
|
this.$refs.searchValue.focusInput();
|
|
|
|
this.$emit('shown');
|
|
},
|
|
onHide() {
|
|
this.isVisible = false;
|
|
this.currentCategory = 0;
|
|
this.searchValue = '';
|
|
this.$emit('hidden');
|
|
},
|
|
},
|
|
i18n: {
|
|
addReaction: __('Add reaction'),
|
|
createEmoji: __('Create new emoji'),
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="emoji-picker">
|
|
<gl-disclosure-dropdown
|
|
ref="dropdown"
|
|
:class="dropdownClass"
|
|
:placement="placement"
|
|
@shown="onShow"
|
|
@hidden="onHide"
|
|
>
|
|
<template #toggle>
|
|
<gl-button
|
|
v-gl-tooltip
|
|
:title="$options.i18n.addReaction"
|
|
:class="[toggleClass, { 'is-active': isVisible }]"
|
|
class="gl-relative gl-h-full add-reaction-button btn-icon"
|
|
data-testid="add-reaction-button"
|
|
>
|
|
<slot name="button-content">
|
|
<span class="reaction-control-icon reaction-control-icon-neutral">
|
|
<gl-icon class="award-control-icon-neutral gl-button-icon" name="slight-smile" />
|
|
</span>
|
|
<span class="reaction-control-icon reaction-control-icon-positive">
|
|
<gl-icon
|
|
class="award-control-icon-positive gl-button-icon !gl-left-3"
|
|
name="smiley"
|
|
/>
|
|
</span>
|
|
<span class="reaction-control-icon reaction-control-icon-super-positive">
|
|
<gl-icon
|
|
class="award-control-icon-super-positive gl-button-icon !gl-left-3"
|
|
name="smile"
|
|
/>
|
|
</span>
|
|
</slot>
|
|
</gl-button>
|
|
</template>
|
|
|
|
<template #header>
|
|
<gl-search-box-by-type
|
|
ref="searchValue"
|
|
v-model="searchValue"
|
|
class="add-reaction-search gl-border-b-1 gl-border-b-solid gl-border-b-gray-200"
|
|
borderless
|
|
autofocus
|
|
debounce="500"
|
|
:aria-label="__('Search for an emoji')"
|
|
@input="onSearchInput"
|
|
/>
|
|
</template>
|
|
|
|
<div
|
|
v-show="!searchValue"
|
|
class="award-list gl-display-flex gl-border-b-solid gl-border-gray-100 gl-border-b-1"
|
|
>
|
|
<gl-button
|
|
v-for="(category, index) in categoryNames"
|
|
:key="category.name"
|
|
category="tertiary"
|
|
:class="{ 'emoji-picker-category-active': index === currentCategory }"
|
|
class="gl-px-3! gl-rounded-0! gl-border-b-2! gl-border-b-solid! gl-flex-grow-1 emoji-picker-category-tab"
|
|
:icon="category.icon"
|
|
:aria-label="category.name"
|
|
@click="scrollToCategory(category.name)"
|
|
@keydown.enter="scrollToCategory(category.name)"
|
|
/>
|
|
</div>
|
|
<emoji-list v-if="isVisible" :search-value="searchValue">
|
|
<template #default="{ filteredCategories }">
|
|
<virtual-list
|
|
ref="virtualScoller"
|
|
:size="258"
|
|
:remain="1"
|
|
:bench="2"
|
|
variable
|
|
:onscroll="onScroll"
|
|
>
|
|
<div
|
|
v-for="(category, categoryKey) in filteredCategories"
|
|
:key="categoryKey"
|
|
:style="{ height: category.height + 'px' }"
|
|
>
|
|
<category :category="categoryKey" :emojis="category.emojis" @click="selectEmoji" />
|
|
</div>
|
|
</virtual-list>
|
|
</template>
|
|
</emoji-list>
|
|
|
|
<template v-if="newCustomEmojiPath" #footer>
|
|
<div
|
|
class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-display-flex gl-flex-direction-column gl-p-2! gl-pt-0!"
|
|
>
|
|
<gl-button
|
|
:href="newCustomEmojiPath"
|
|
category="tertiary"
|
|
block
|
|
data-testid="create-new-emoji"
|
|
class="gl-justify-content-start! gl-mt-2!"
|
|
>
|
|
{{ $options.i18n.createEmoji }}
|
|
</gl-button>
|
|
</div>
|
|
</template>
|
|
</gl-disclosure-dropdown>
|
|
</div>
|
|
</template>
|