diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/emoji | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/emoji')
-rw-r--r-- | app/assets/javascripts/emoji/components/category.vue | 61 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/emoji_group.vue | 35 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/emoji_list.vue | 44 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/picker.vue | 121 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/utils.js | 27 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/constants.js | 14 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/index.js | 16 |
7 files changed, 308 insertions, 10 deletions
diff --git a/app/assets/javascripts/emoji/components/category.vue b/app/assets/javascripts/emoji/components/category.vue new file mode 100644 index 00000000000..a11122d5403 --- /dev/null +++ b/app/assets/javascripts/emoji/components/category.vue @@ -0,0 +1,61 @@ +<script> +import { GlIntersectionObserver } from '@gitlab/ui'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import EmojiGroup from './emoji_group.vue'; + +export default { + components: { + GlIntersectionObserver, + EmojiGroup, + }, + props: { + category: { + type: String, + required: true, + }, + emojis: { + type: Array, + required: true, + }, + }, + data() { + return { + renderGroup: false, + }; + }, + computed: { + categoryTitle() { + return capitalizeFirstCharacter(this.category); + }, + }, + methods: { + categoryAppeared() { + this.renderGroup = true; + this.$emit('appear', this.category); + }, + categoryDissappeared() { + this.renderGroup = false; + }, + }, +}; +</script> + +<template> + <gl-intersection-observer class="gl-px-5 gl-h-full" @appear="categoryAppeared"> + <div class="gl-top-0 gl-py-3 gl-w-full emoji-picker-category-header"> + <b>{{ categoryTitle }}</b> + </div> + <template v-if="emojis.length"> + <emoji-group + v-for="(emojiGroup, index) in emojis" + :key="index" + :emojis="emojiGroup" + :render-group="renderGroup" + :click-emoji="(emoji) => $emit('click', emoji)" + /> + </template> + <p v-else> + {{ s__('AwardEmoji|No emojis found.') }} + </p> + </gl-intersection-observer> +</template> diff --git a/app/assets/javascripts/emoji/components/emoji_group.vue b/app/assets/javascripts/emoji/components/emoji_group.vue new file mode 100644 index 00000000000..539cd6963b1 --- /dev/null +++ b/app/assets/javascripts/emoji/components/emoji_group.vue @@ -0,0 +1,35 @@ +<script> +export default { + props: { + emojis: { + type: Array, + required: true, + }, + renderGroup: { + type: Boolean, + required: true, + }, + clickEmoji: { + type: Function, + required: true, + }, + }, +}; +</script> + +<template functional> + <div class="gl-display-flex gl-flex-wrap gl-mb-2"> + <template v-if="props.renderGroup"> + <button + v-for="emoji in props.emojis" + :key="emoji" + type="button" + class="gl-border-0 gl-bg-transparent gl-px-0 gl-py-2 gl-text-center emoji-picker-emoji" + data-testid="emoji-button" + @click="props.clickEmoji(emoji)" + > + <gl-emoji :data-name="emoji" /> + </button> + </template> + </div> +</template> diff --git a/app/assets/javascripts/emoji/components/emoji_list.vue b/app/assets/javascripts/emoji/components/emoji_list.vue new file mode 100644 index 00000000000..0d73d751c6d --- /dev/null +++ b/app/assets/javascripts/emoji/components/emoji_list.vue @@ -0,0 +1,44 @@ +<script> +import { chunk } from 'lodash'; +import { searchEmoji } from '~/emoji'; +import { EMOJIS_PER_ROW } from '../constants'; +import { getEmojiCategories, generateCategoryHeight } from './utils'; + +export default { + props: { + searchValue: { + type: String, + required: true, + }, + }, + data() { + return { render: false }; + }, + computed: { + filteredCategories() { + if (this.searchValue !== '') { + const emojis = chunk( + searchEmoji(this.searchValue).map(({ emoji }) => emoji.name), + EMOJIS_PER_ROW, + ); + + return { + search: { emojis, height: generateCategoryHeight(emojis.length) }, + }; + } + + return this.categories; + }, + }, + async mounted() { + this.categories = await getEmojiCategories(); + this.render = true; + }, +}; +</script> + +<template> + <div v-if="render"> + <slot :filtered-categories="filteredCategories"></slot> + </div> +</template> diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue new file mode 100644 index 00000000000..7cd20d82329 --- /dev/null +++ b/app/assets/javascripts/emoji/components/picker.vue @@ -0,0 +1,121 @@ +<script> +import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; +import VirtualList from 'vue-virtual-scroll-list'; +import { CATEGORY_NAMES } from '~/emoji'; +import { CATEGORY_ICON_MAP } from '../constants'; +import Category from './category.vue'; +import EmojiList from './emoji_list.vue'; +import { getEmojiCategories } from './utils'; + +export default { + components: { + GlIcon, + GlDropdown, + GlSearchBoxByType, + VirtualList, + Category, + EmojiList, + }, + props: { + toggleClass: { + type: [Array, String, Object], + required: false, + default: () => [], + }, + }, + data() { + return { + currentCategory: null, + searchValue: '', + }; + }, + computed: { + categoryNames() { + return CATEGORY_NAMES.map((category) => ({ + name: category, + icon: CATEGORY_ICON_MAP[category], + })); + }, + }, + methods: { + categoryAppeared(category) { + this.currentCategory = category; + }, + async scrollToCategory(categoryName) { + const categories = await getEmojiCategories(); + const { top } = categories[categoryName]; + + this.$refs.virtualScoller.setScrollTop(top); + }, + selectEmoji(name) { + this.$emit('click', name); + this.$refs.dropdown.hide(); + }, + getBoundaryElement() { + return document.querySelector('.content-wrapper') || 'scrollParent'; + }, + onSearchInput() { + this.$refs.virtualScoller.setScrollTop(0); + this.$refs.virtualScoller.forceRender(); + }, + }, +}; +</script> + +<template> + <div class="emoji-picker"> + <gl-dropdown + ref="dropdown" + :toggle-class="toggleClass" + :boundary="getBoundaryElement()" + menu-class="dropdown-extended-height" + no-flip + right + lazy + > + <template #button-content><slot name="button-content"></slot></template> + <gl-search-box-by-type + v-model="searchValue" + class="gl-mx-5! gl-mb-2!" + autofocus + debounce="500" + @input="onSearchInput" + /> + <div + v-show="!searchValue" + class="gl-display-flex gl-mx-5 gl-border-b-solid gl-border-gray-100 gl-border-b-1" + > + <button + v-for="category in categoryNames" + :key="category.name" + :class="{ + 'gl-text-black-normal! emoji-picker-category-active': category.name === currentCategory, + }" + type="button" + class="gl-border-0 gl-border-b-2 gl-border-b-solid gl-flex-fill-1 gl-text-gray-300 gl-pt-3 gl-pb-3 gl-bg-transparent emoji-picker-category-tab" + @click="scrollToCategory(category.name)" + > + <gl-icon :name="category.icon" :size="12" /> + </button> + </div> + <emoji-list :search-value="searchValue"> + <template #default="{ filteredCategories }"> + <virtual-list ref="virtualScoller" :size="258" :remain="1" :bench="2" variable> + <div + v-for="(category, categoryKey) in filteredCategories" + :key="categoryKey" + :style="{ height: category.height + 'px' }" + > + <category + :category="categoryKey" + :emojis="category.emojis" + @appear="categoryAppeared" + @click="selectEmoji" + /> + </div> + </virtual-list> + </template> + </emoji-list> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/emoji/components/utils.js b/app/assets/javascripts/emoji/components/utils.js new file mode 100644 index 00000000000..b95b56a1d6f --- /dev/null +++ b/app/assets/javascripts/emoji/components/utils.js @@ -0,0 +1,27 @@ +import { chunk, memoize } from 'lodash'; +import { initEmojiMap, getEmojiCategoryMap } from '~/emoji'; +import { EMOJIS_PER_ROW, EMOJI_ROW_HEIGHT, CATEGORY_ROW_HEIGHT } from '../constants'; + +export const generateCategoryHeight = (emojisLength) => + emojisLength * EMOJI_ROW_HEIGHT + CATEGORY_ROW_HEIGHT; + +export const getEmojiCategories = memoize(async () => { + await initEmojiMap(); + + const categories = await getEmojiCategoryMap(); + let top = 0; + + return Object.freeze( + Object.keys(categories).reduce((acc, category) => { + const emojis = chunk(categories[category], EMOJIS_PER_ROW); + const height = generateCategoryHeight(emojis.length); + const newAcc = { + ...acc, + [category]: { emojis, height, top }, + }; + top += height; + + return newAcc; + }, {}), + ); +}); diff --git a/app/assets/javascripts/emoji/constants.js b/app/assets/javascripts/emoji/constants.js new file mode 100644 index 00000000000..bf73d1ca5a9 --- /dev/null +++ b/app/assets/javascripts/emoji/constants.js @@ -0,0 +1,14 @@ +export const CATEGORY_ICON_MAP = { + activity: 'dumbbell', + people: 'smiley', + nature: 'nature', + food: 'food', + travel: 'car', + objects: 'object', + symbols: 'heart', + flags: 'flag', +}; + +export const EMOJIS_PER_ROW = 9; +export const EMOJI_ROW_HEIGHT = 34; +export const CATEGORY_ROW_HEIGHT = 37; diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index d022fcbeabe..d3b658a4020 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -2,6 +2,7 @@ import { escape, minBy } from 'lodash'; import emojiAliases from 'emojis/aliases.json'; import AccessorUtilities from '../lib/utils/accessor'; import axios from '../lib/utils/axios_utils'; +import { CATEGORY_ICON_MAP } from './constants'; let emojiMap = null; let validEmojiNames = null; @@ -155,19 +156,14 @@ export function sortEmoji(items) { return [...items].sort((a, b) => a.score - b.score || a.fieldValue.localeCompare(b.fieldValue)); } +export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP); + let emojiCategoryMap; export function getEmojiCategoryMap() { if (!emojiCategoryMap) { - emojiCategoryMap = { - activity: [], - people: [], - nature: [], - food: [], - travel: [], - objects: [], - symbols: [], - flags: [], - }; + emojiCategoryMap = CATEGORY_NAMES.reduce((acc, category) => { + return { ...acc, [category]: [] }; + }, {}); Object.keys(emojiMap).forEach((name) => { const emoji = emojiMap[name]; if (emojiCategoryMap[emoji.c]) { |