UNPKG

@dialpad/dialtone

Version:

Dialpad's Dialtone design system monorepo

1 lines 63 kB
{"version":3,"file":"emoji_picker-Cl-T4BXK.cjs","names":["$emit","ARROW_KEYS","$refs"],"sources":["../components/emoji_picker/modules/emoji_search.vue","../components/emoji_picker/modules/emoji_tabset.vue","../components/emoji_picker/composables/useKeyboardNavigation.js","../components/emoji_picker/modules/emoji_selector.vue","../components/emoji_picker/modules/emoji_skin_selector.vue","../components/emoji_picker/modules/emoji_description.vue","../components/emoji_picker/emoji_picker.vue"],"sourcesContent":["<template>\n <div class=\"d-emoji-picker__search d-emoji-picker__alignment\">\n <dt-input\n id=\"searchInput\"\n ref=\"searchInput\"\n :placeholder=\"searchPlaceholderLabel\"\n :model-value=\"modelValue\"\n @update:model-value=\"$emit('update:modelValue', $event)\"\n @keydown.up=\"$emit('focus-tabset')\"\n @keydown.down.prevent=\"$emit('focus-emoji-selector')\"\n @keydown.enter=\"$emit('select-first-emoji')\"\n >\n <template #leftIcon>\n <dt-icon-search\n size=\"200\"\n />\n </template>\n <template\n v-if=\"modelValue.length > 0\"\n #rightIcon\n >\n <dt-button\n importance=\"clear\"\n size=\"xs\"\n class=\"d-emoji-picker__search-x-button\"\n circle\n kind=\"muted\"\n @click=\"clearSearch\"\n >\n <template #icon>\n <dt-icon-close\n size=\"200\"\n />\n </template>\n </dt-button>\n </template>\n </dt-input>\n </div>\n</template>\n\n<script setup>\nimport { DtIconSearch, DtIconClose } from '@dialpad/dialtone-icons/vue3';\nimport { DtInput } from '@/components/input';\nimport { DtButton } from '@/components/button';\nimport { onMounted, ref } from 'vue';\n\ndefineProps({\n searchPlaceholderLabel: {\n type: String,\n required: true,\n },\n modelValue: {\n type: String,\n default: '',\n },\n});\n\nconst emits = defineEmits(['update:modelValue', 'focus-emoji-selector', 'focus-tabset', 'select-first-emoji']);\n\nconst searchInput = ref(null);\n\nfunction clearSearch () {\n emits('update:modelValue', '');\n focusSearchInput();\n}\n\nfunction focusSearchInput () {\n searchInput.value.focus();\n}\nonMounted(() => {\n focusSearchInput();\n});\n\ndefineExpose({\n focusSearchInput,\n});\n</script>\n","<template>\n <div class=\"d-emoji-picker__tabset\">\n <dt-tab-group\n :selected=\"selectedTab\"\n size=\"sm\"\n tab-list-class=\"d-emoji-picker__tabset-list\"\n >\n <template #tabs>\n <dt-tab\n v-for=\"(tab, index) in tabs\"\n :id=\"tab.id\"\n :key=\"tab.id\"\n :ref=\"el => { if (el) setTabsetRef(el) }\"\n :label=\"tab.label\"\n :panel-id=\"tab.panelId\"\n :tabindex=\"index + 1\"\n aria-controls=\"d-emoji-picker-list\"\n @keydown=\"handleKeyDown($event, tab.id)\"\n @click.capture.stop=\"selectTabset(tab.id)\"\n >\n <component\n :is=\"tab.icon\"\n size=\"400\"\n />\n </dt-tab>\n </template>\n </dt-tab-group>\n </div>\n</template>\n\n<script setup>\nimport { computed, ref, watch } from 'vue';\nimport { DtTab, DtTabGroup } from '@/components/tab';\nimport { returnFirstEl } from '@/common/utils';\nimport {\n DtIconClock,\n DtIconSatisfied,\n DtIconLivingThing,\n DtIconFood,\n DtIconObject,\n DtIconTransportation,\n DtIconLightbulb,\n DtIconHeart,\n DtIconFlag,\n DtIconDialpadStar,\n} from '@dialpad/dialtone-icons/vue3';\n\nconst props = defineProps({\n /**\n * Whether to show the recently used tab or not\n * @type {Boolean}\n * @default false\n */\n showRecentlyUsedTab: {\n type: Boolean,\n default: false,\n },\n\n /**\n * Whether to show the custom emojis tab or not\n * @type {Boolean}\n * @default false\n */\n showCustomEmojisTab: {\n type: Boolean,\n default: false,\n },\n\n scrollIntoTab: {\n type: Number,\n required: true,\n },\n\n emojiFilter: {\n type: String,\n default: '',\n },\n\n /**\n * The labels for the aria-label\n * @type {Array}\n * @required\n */\n tabSetLabels: {\n type: Array,\n required: true,\n },\n});\n\nconst emits = defineEmits([\n /**\n * Emitted when a tab is selected\n * @event selected-tabset\n * @param {String} tabId - The name of the tab that was selected\n */\n 'selected-tabset',\n\n 'focus-search-input',\n 'focus-skin-selector',\n]);\n\nconst TABS_DATA = [\n { label: props.tabSetLabels[0], icon: DtIconClock },\n { label: props.tabSetLabels[1], icon: DtIconSatisfied },\n { label: props.tabSetLabels[2], icon: DtIconLivingThing },\n { label: props.tabSetLabels[3], icon: DtIconFood },\n { label: props.tabSetLabels[4], icon: DtIconObject },\n { label: props.tabSetLabels[5], icon: DtIconTransportation },\n { label: props.tabSetLabels[6], icon: DtIconLightbulb },\n { label: props.tabSetLabels[7], icon: DtIconHeart },\n { label: props.tabSetLabels[8], icon: DtIconFlag },\n { label: props.tabSetLabels[9], icon: DtIconDialpadStar },\n];\n\nconst tabs = computed(() => {\n const tabsData = props.showRecentlyUsedTab ? TABS_DATA : TABS_DATA.slice(1);\n // if showCustomEmojisTab is false remove last index of TABS_DATA\n if (!props.showCustomEmojisTab) {\n tabsData.pop();\n }\n\n return tabsData.map((tab, index) => ({\n ...tab,\n // IDs on dt-tab component need to be on string\n id: (index + 1).toString(),\n panelId: (index + 1).toString(),\n }));\n});\n\nconst isSearching = computed(() => props.emojiFilter.length > 0);\n\nconst selectedTab = ref('1');\n\nconst tabsetRef = ref([]);\n\nwatch(() => props.scrollIntoTab,\n () => {\n if (!isSearching.value) {\n selectedTab.value = (props.scrollIntoTab + 1).toString();\n }\n });\n\nwatch(isSearching,\n () => {\n if (isSearching.value) {\n selectedTab.value = null;\n }\n });\n\n/**\n * We are using .capture.stop modifiers on the click event\n * because we don't want to trigger the click event of the\n * dt-tab component\n */\nfunction selectTabset (id) {\n // IDs on scrollToTab need to be on number\n const parseId = parseInt(id);\n // IDs on dt-tab component need to be on string\n selectedTab.value = id;\n emits('selected-tabset', parseId);\n}\n\nfunction setTabsetRef (ref) {\n // We push the $el, because $el is the button inside the dt-tab component\n // and we need the button to focus it\n tabsetRef.value.push(returnFirstEl(ref.$el));\n}\n\nfunction focusTabset () {\n tabsetRef.value[0].focus();\n}\n\nfunction handleKeyDown (event, tabId) {\n if (event.key === 'Enter') {\n selectTabset(tabId);\n // We blur because seems like the tab component override the selected prop, and it removes the selected style\n tabsetRef.value[tabId - 1].blur();\n }\n\n if (event.key === 'Tab') {\n event.preventDefault();\n if (event.shiftKey) {\n emits('focus-skin-selector');\n } else {\n emits('focus-search-input');\n }\n }\n\n if (event.key === 'ArrowDown') {\n // Jump to search input\n emits('focus-search-input');\n }\n}\n\ndefineExpose({\n focusTabset,\n});\n</script>\n","import { ref } from 'vue';\nimport { EMOJIS_PER_ROW, ARROW_KEYS } from '@/components/emoji_picker/emoji_picker_constants';\n\nexport function useKeyboardNavigation () {\n const emojiRefs = ref([]);\n const emojiFilteredRefs = ref([]);\n const isFiltering = ref(false);\n const hoverFirstEmoji = ref(true);\n\n function _handleArrowLeft (indexTab, indexEmoji) {\n if (!focusEmoji(indexTab, indexEmoji - 1)) {\n if (emojiRefs.value[indexTab - 1]) {\n focusEmoji(indexTab - 1, emojiRefs.value[indexTab - 1].length - 1);\n } else {\n focusEmoji(emojiRefs.value.length - 1, emojiRefs.value[emojiRefs.value.length - 1].length - 1);\n }\n }\n }\n\n function _handleArrowRight (indexTab, indexEmoji) {\n if (!focusEmoji(indexTab, indexEmoji + 1)) {\n if (!focusEmoji(indexTab + 1, 0)) {\n focusEmoji(0, 0);\n }\n }\n }\n\n function _handleArrowLeftFiltered (indexTab, indexEmoji) {\n if (!focusEmoji(0, indexEmoji - 1)) {\n focusEmoji(0, emojiFilteredRefs.value.length - 1);\n }\n }\n\n function _handleArrowRightFiltered (indexTab, indexEmoji) {\n if (!focusEmoji(0, indexEmoji + 1)) {\n focusEmoji(0, 0);\n }\n }\n\n function _handleHorizontalNavigation (direction, indexTab, indexEmoji) {\n if (isFiltering.value) {\n if (direction === 'left') {\n _handleArrowLeftFiltered(indexTab, indexEmoji);\n } else if (direction === 'right') {\n _handleArrowRightFiltered(indexTab, indexEmoji);\n }\n } else {\n if (direction === 'left') {\n _handleArrowLeft(indexTab, indexEmoji);\n } else if (direction === 'right') {\n _handleArrowRight(indexTab, indexEmoji);\n }\n }\n }\n\n function focusEmoji (indexTab, indexEmoji) {\n const emojiRef = isFiltering.value\n ? emojiFilteredRefs.value?.[indexEmoji]\n : emojiRefs.value?.[indexTab]?.[indexEmoji];\n\n if (emojiRef) {\n emojiRef.focus();\n return true;\n }\n\n return false;\n }\n\n function setEmojiRef (el, indexTab, indexEmoji) {\n if (!emojiRefs.value[indexTab]) {\n emojiRefs.value[indexTab] = [];\n }\n emojiRefs.value[indexTab][indexEmoji] = el;\n }\n\n function setFilteredRef (el, index) {\n emojiFilteredRefs.value[index] = el;\n }\n\n function handleArrowNavigationFiltered (key, indexEmoji) {\n hoverFirstEmoji.value = false;\n\n if (key === ARROW_KEYS.ARROW_UP) {\n const position = indexEmoji % EMOJIS_PER_ROW;\n\n if (!focusEmoji(0, indexEmoji - EMOJIS_PER_ROW)) {\n const lastEmojiPosition =\n emojiFilteredRefs.value.length - (emojiFilteredRefs.value.length % EMOJIS_PER_ROW) + position;\n\n focusEmoji(0, lastEmojiPosition);\n\n if (!focusEmoji(0, lastEmojiPosition)) {\n focusEmoji(0, emojiFilteredRefs.value.length - 1);\n }\n }\n }\n\n if (key === ARROW_KEYS.ARROW_DOWN) {\n if (!focusEmoji(0, indexEmoji + EMOJIS_PER_ROW)) {\n const position = indexEmoji % EMOJIS_PER_ROW;\n\n if (emojiFilteredRefs.value?.[indexEmoji + (EMOJIS_PER_ROW - position)]) {\n focusEmoji(0, emojiFilteredRefs.value.length - 1);\n } else {\n focusEmoji(0, position);\n }\n }\n }\n\n if (key === ARROW_KEYS.ARROW_LEFT) {\n _handleHorizontalNavigation('left', 0, indexEmoji);\n }\n\n if (key === ARROW_KEYS.ARROW_RIGHT) {\n _handleHorizontalNavigation('right', 0, indexEmoji);\n }\n }\n\n function handleArrowNavigation (key, indexTab, indexEmoji) {\n if (key === 'ArrowUp') {\n const position = indexEmoji % EMOJIS_PER_ROW;\n\n if (indexTab === 0) {\n // we are on the first emoji tab, then we should jump to the last row of the last emoji tab\n const numberOfMissingEmojis =\n EMOJIS_PER_ROW - (emojiRefs.value[emojiRefs.value.length - 1].length % EMOJIS_PER_ROW);\n\n const emojiToJump =\n emojiRefs.value[emojiRefs.value.length - 1].length + numberOfMissingEmojis - (EMOJIS_PER_ROW - position);\n\n if (!focusEmoji(emojiRefs.value.length - 1, emojiToJump)) {\n // if there is no emoji in this position, jump to the last emoji of the row\n focusEmoji(emojiRefs.value.length - 1, emojiRefs.value[emojiRefs.value.length - 1].length - 1);\n }\n return;\n }\n\n // if we are not on the first tab, we should jump to the previous row of the current tab\n if (!focusEmoji(indexTab, indexEmoji - EMOJIS_PER_ROW)) {\n // if there is no previous row, we should jump to emoji in the sampe position of the previous tab\n const previousTab = indexTab - 1 < 0 ? 0 : indexTab - 1;\n const emojisInPreviousTab = emojiRefs.value[previousTab].length;\n const lastEmojiPosition = emojisInPreviousTab - (emojisInPreviousTab % EMOJIS_PER_ROW) + position;\n\n if (!focusEmoji(previousTab, lastEmojiPosition)) {\n // if there is no emoji in this position, jump to the last emoji of the row\n focusEmoji(indexTab - 1, emojiRefs.value[indexTab - 1].length - 1);\n }\n }\n }\n\n if (key === 'ArrowDown') {\n if (!focusEmoji(indexTab, indexEmoji + EMOJIS_PER_ROW)) {\n // if cannot go down\n\n // Calculate position from cell 0 to cell 8\n const position = indexEmoji % EMOJIS_PER_ROW;\n\n // check if it exists a next row in the current tab\n if (emojiRefs.value?.[indexTab]?.[indexEmoji + (EMOJIS_PER_ROW - position)]) {\n // if it exists, we should focus the last emoji of the next row in the current tab\n focusEmoji(indexTab, emojiRefs.value[indexTab].length - 1);\n // if we are at the end of the list it will do nothing\n } else {\n // We don't have next row, we are in the last of the tab, then jump\n // to the next tab but in the equal emoji position in row 0.\n\n if (!focusEmoji(indexTab + 1, position)) {\n // We are on the bottom!, should jump to the same position emoji in the first row of the first tabset\n // if it doesn't has, jump to the last\n if (!focusEmoji(0, position)) {\n focusEmoji(0, emojiRefs.value[0].length - 1);\n }\n }\n }\n }\n }\n\n if (key === 'ArrowLeft') {\n _handleHorizontalNavigation('left', indexTab, indexEmoji);\n }\n\n if (key === 'ArrowRight') {\n _handleHorizontalNavigation('right', indexTab, indexEmoji);\n }\n }\n\n return {\n emojiFilteredRefs,\n isFiltering,\n hoverFirstEmoji,\n setEmojiRef,\n setFilteredRef,\n focusEmoji,\n handleArrowNavigationFiltered,\n handleArrowNavigation,\n };\n}\n","<template>\n <div\n class=\"d-emoji-picker__selector\"\n >\n <div\n id=\"d-emoji-picker-list\"\n ref=\"listRef\"\n class=\"d-emoji-picker__list\"\n >\n <p\n v-if=\"emojiFilter\"\n class=\"d-emoji-picker__search-label d-emoji-picker__alignment\"\n >\n {{ filteredEmojis.length > 0 ? searchResultsLabel : searchNoResultsLabel }}\n </p>\n <div\n v-else\n ref=\"tabCategoryRef\"\n class=\"d-emoji-picker__category d-emoji-picker__alignment\"\n >\n <p>\n {{ fixedLabel }}\n </p>\n </div>\n <div\n v-for=\"(tabLabel, indexTab) in tabLabels\"\n v-show=\"!emojiFilter\"\n :key=\"indexTab\"\n :ref=\"tabLabel.ref\"\n class=\"d-emoji-picker__alignment\"\n >\n <p\n v-if=\"indexTab\"\n >\n {{ tabLabel.label }}\n </p>\n <div\n class=\"d-emoji-picker__tab\"\n >\n <button\n v-for=\"(emoji, indexEmoji) in\n (emojis[tabs[indexTab] + skinTone] ? emojis[tabs[indexTab] + skinTone] : emojis[tabs[indexTab]])\"\n :key=\"emoji.shortname\"\n :ref=\"el => { if (el) setEmojiRef(el, indexTab, indexEmoji) }\"\n type=\"button\"\n :aria-label=\"emoji.name\"\n @click=\"event => selectEmoji(emoji, event)\"\n @focusin=\"highlightEmoji(emoji)\"\n @focusout=\"highlightEmoji(null)\"\n @mouseover=\"highlightEmoji(emoji)\"\n @mouseleave=\"highlightEmoji(null)\"\n @keydown=\"event => handleKeyDown(event, indexTab, indexEmoji, emoji)\"\n >\n <img\n class=\"d-icon d-icon--size-500\"\n :alt=\"emoji.name\"\n :aria-label=\"emoji.name\"\n :title=\"emoji.name\"\n :src=\"getImgSrc(emoji)\"\n @error=\"handleImageError\"\n >\n </button>\n </div>\n </div>\n <div\n v-if=\"emojiFilter\"\n class=\"d-emoji-picker__alignment\"\n >\n <div\n class=\"d-emoji-picker__tab \"\n data-qa=\"filtered-emojis\"\n >\n <button\n v-for=\"(emoji, index) in filteredEmojis\"\n :key=\"emoji.shortname\"\n :ref=\"el => { if (el) setFilteredRef(el, index) }\"\n type=\"button\"\n :aria-label=\"emoji.name\"\n :class=\"{\n 'hover-emoji': (index === 0 && hoverFirstEmoji),\n }\"\n @click=\"event => selectEmoji(emoji, event)\"\n @focusin=\"highlightEmoji(emoji)\"\n @focusout=\"highlightEmoji(null)\"\n @mouseover=\"hoverEmoji(emoji)\"\n @mouseleave=\"hoverEmoji(null)\"\n @keydown=\"event => handleKeyDownFilteredEmojis(event, index, emoji)\"\n >\n <img\n class=\"d-icon d-icon--size-500\"\n :alt=\"emoji.name\"\n :aria-label=\"emoji.name\"\n :title=\"emoji.name\"\n :src=\"`${CDN_URL + emoji.unicode_character}.png`\"\n >\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup>\n/* eslint-disable max-lines */\nimport { emojisGrouped as emojis } from '@dialpad/dialtone-emojis';\nimport { computed, onMounted, onBeforeUnmount, ref, watch, nextTick } from 'vue';\nimport { CDN_URL, ARROW_KEYS } from '@/components/emoji_picker/emoji_picker_constants';\nimport { useKeyboardNavigation } from '@/components/emoji_picker/composables/useKeyboardNavigation';\n\nconst props = defineProps({\n /**\n * The filter to apply to the emoji list\n * @type {String}\n * @default ''\n */\n emojiFilter: {\n type: String,\n default: '',\n },\n\n /**\n * The skin tone to apply to the emoji list\n * @type {String}\n * @required\n */\n skinTone: {\n type: String,\n required: true,\n },\n\n /**\n * The labels for the tabset\n * @type {Array}\n * @required\n */\n tabsetLabels: {\n type: Array,\n required: true,\n },\n\n selectedTabset: {\n type: Object,\n required: true,\n },\n\n /**\n * The label for the search results tab\n * @type {String}\n * @required\n */\n searchResultsLabel: {\n type: String,\n required: true,\n },\n\n searchNoResultsLabel: {\n type: String,\n required: true,\n },\n\n /**\n * The list of recently used emojis\n * @type {Array}\n */\n recentlyUsedEmojis: {\n type: Array,\n default: () => [],\n },\n\n /**\n * The list of custom emojis\n * @type {Array}\n */\n customEmojis: {\n type: Array,\n default: () => [],\n },\n});\n\nconst emits = defineEmits([\n /**\n * Emitted when the user hover over an emoji\n * @event highlighted-emoji\n * @param {Object} emoji - The emoji data that was hovered\n */\n 'highlighted-emoji',\n\n /**\n * Emitted when the user select an emoji\n * @event selected-emoji\n * @param {Object} emoji - The emoji data that was selected\n */\n 'selected-emoji',\n\n /**\n * Emitted when the user scroll into an emoji tab\n * @event scroll-into-tab\n * @param {Number} tab-index - The tab that was scrolled into\n */\n 'scroll-into-tab',\n\n /**\n * Emitted when the user reach bottom scroll\n * This event is used on handleScroll method\n * @event scroll-bottom-reached\n */\n 'scroll-bottom-reached',\n\n /**\n * Emitted when the user reach the end of the emoji list\n * @event focus-skin-selector\n */\n 'focus-skin-selector',\n\n /**\n * Emitted when the user shift tab in first tab of emoji selector\n * @event focus-search-input\n */\n 'focus-search-input',\n]);\n\nconst {\n emojiFilteredRefs,\n isFiltering,\n hoverFirstEmoji,\n setEmojiRef,\n setFilteredRef,\n focusEmoji,\n handleArrowNavigationFiltered,\n handleArrowNavigation,\n} = useKeyboardNavigation();\n\n/**\n * The ref for the tab category\n * This is used to display the fixed label\n */\nconst tabCategoryRef = ref(null);\n\n/**\n * The ref for the list\n * This is used to display the tabs\n */\nconst listRef = ref(null);\n\n/**\n * The ref for the tab label observer\n * This is used to update the fixed label\n */\nconst tabLabelObserver = ref(null);\n\n/**\n * The list of tabs\n * This is used to display the tabs\n */\nconst TABS_DATA = ['Recently used', 'People', 'Nature', 'Food', 'Activity', 'Travel', 'Objects', 'Symbols', 'Flags', 'Custom'];\n\n/**\n * The list of tab labels\n * This is used to display the tabs\n * This is a computed property because it will check if the recently used emojis or custom emojis list is empty\n * If it is empty, it will remove it\n */\nconst tabLabels = computed(() => {\n let updateTabLabels = props.tabsetLabels.map((label) => ({ label, ref: ref(null) }));\n\n if (props.recentlyUsedEmojis && !props.recentlyUsedEmojis.length) {\n updateTabLabels = props.tabsetLabels.slice(1).map((label) => ({ label, ref: ref(null) }));\n }\n\n if (props.customEmojis && !props.customEmojis.length) {\n updateTabLabels.pop();\n }\n\n return updateTabLabels;\n});\n\n/**\n * The label of the fixed tab\n * This is used to display the fixed label\n */\nconst fixedLabel = ref(tabLabels.value[0].label);\n\n/**\n * The list of tabs\n * This is used to display the tabs\n * This is a computed property because it will check if the recently used emojis list or custom emojis is empty\n * If it is empty, it will remove it\n * The difference between this and the tab labels is that this one will set the structure of tabs\n * and the tab labels will set the labels\n */\nconst tabs = computed(() => {\n const updateTabsOrder = props.recentlyUsedEmojis.length ? TABS_DATA.slice() : TABS_DATA.slice(1);\n\n if (props.customEmojis && !props.customEmojis.length) {\n updateTabsOrder.pop();\n }\n\n return updateTabsOrder;\n});\n\n/**\n * The list of current emojis that match the filter\n * This will be updated when the emojiFilter changes\n * This is used to display the search results\n * The difference between this and the current emojis list is that this one will not have the skin tone applied\n */\nconst filteredEmojis = ref([]);\n\n/**\n * The current emojis list we are displaying\n * This will be updated when the skin tone changes\n * The difference between this and the emojis list is that this one will have only the skin tone applied\n */\nconst currentEmojis = computed(() => {\n return [\n ...emojis[`People${props.skinTone}`],\n ...emojis.Nature,\n ...emojis.Food,\n ...emojis[`Activity${props.skinTone}`],\n ...emojis.Travel,\n ...emojis[`Objects${props.skinTone}`],\n ...emojis.Symbols,\n ...emojis.Flags,\n ];\n});\n\n/**\n * This will trigger the searchByNameAndKeywords function with debounce of 300 milliseconds\n */\nconst debouncedSearch = debounce(() => {\n // We clean the emojiFilteredRefs to have an updated ref list for the search results\n emojiFilteredRefs.value = [];\n searchByNameAndKeywords();\n});\n\n/**\n * handleScroll will be defined when user scroll\n */\nconst handleScroll = () => {\n const container = listRef.value;\n // TODO -- this will probably need to be updated if we add more emojis.\n // because the container height will change.\n // maybe with a nextTick similar of scrollToTab.\n if (container.scrollTop + container.clientHeight >= container.scrollHeight) {\n emits('scroll-bottom-reached');\n }\n};\n\n/**\n * Update the current emojis list on skin tone changes\n * Also update the filtered emojis list\n * @listens skinTone\n */\nwatch(currentEmojis, () => {\n searchByNameAndKeywords();\n}, { immediate: true });\n\n/**\n * Update the recently used emojis list on recently used emojis prop changes\n * @listens recentlyUsedEmojis\n */\nwatch(() => props.recentlyUsedEmojis,\n () => {\n emojis['Recently used'] = props.recentlyUsedEmojis;\n }, { immediate: true });\n\n/**\n * Update the custom emojis list on custom emojis prop changes\n * @listens customEmojis\n */\nwatch(() => props.customEmojis,\n () => {\n emojis.Custom = props.customEmojis;\n }, { immediate: true });\n\n/**\n * Search for emojis by name and keywords\n * Will update the filtered emojis list on emojiFilter update\n * @listens emojiFilter\n */\nwatch(() => props.emojiFilter, () => {\n resetScroll();\n if (props.emojiFilter) {\n isFiltering.value = true;\n } else {\n isFiltering.value = false;\n // If the emoji filter is empty, emit null to remove the highlighted emoji\n // of the previous search\n highlightEmoji(null);\n }\n debouncedSearch();\n});\n\nwatch(\n () => props.selectedTabset,\n (tab) => {\n scrollToTab(tab.tabId);\n },\n { deep: true },\n);\n\nfunction hoverEmoji (emoji, isFirst = false) {\n hoverFirstEmoji.value = isFirst;\n emits('highlighted-emoji', emoji);\n}\n\n/**\n * Filters an array of emoji objects based on a search string that matches both the name and keywords.\n * Will update the filtered emojis list\n */\nfunction searchByNameAndKeywords () {\n const searchStr = props.emojiFilter.toLowerCase();\n filteredEmojis.value = currentEmojis.value.filter(obj => {\n const nameIncludesSearchStr = obj.name.toLowerCase().includes(searchStr);\n const keywordsIncludeSearchStr = obj.keywords.some(keyword => keyword.toLowerCase().includes(searchStr));\n return nameIncludesSearchStr || keywordsIncludeSearchStr;\n });\n nextTick(() => {\n if (searchStr) {\n hoverEmoji(filteredEmojis.value[0], true);\n }\n });\n}\n\nfunction debounce (fn, delay = 300) {\n let timeout;\n\n return (...args) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), delay);\n };\n}\n\nfunction getImgSrc (emoji) {\n // TODO Update json structure to have a property for custom emojis and avoid using date_added\n if (emoji.date_added) { // if custom emoji\n return emoji.image;\n } else { // if regular emoji\n return CDN_URL + emoji.unicode_character + '.png';\n }\n}\n\n/**\n * Handle image error - We hide the entire button if the image is not found\n */\nfunction handleImageError (event) {\n event.target.parentNode.style.display = 'none';\n}\n\n/**\n * Scroll to the selected tab\n */\nfunction scrollToTab (tabIndex, focusFirstEmoji = true) {\n const tabLabel = tabLabels.value[tabIndex - 1];\n const tabElement = tabLabel.ref.value[0];\n\n nextTick(() => {\n const container = listRef.value;\n const offsetTop = tabIndex === 1 ? 0 : tabElement.offsetTop - 15;\n\n container.scrollTop = offsetTop;\n\n if (focusFirstEmoji) {\n focusEmoji((tabIndex - 1), 0);\n }\n });\n}\n\nfunction resetScroll () {\n const container = listRef.value;\n\n container.scrollTop = 0;\n}\n\nfunction setBottomScrollListener () {\n listRef.value.addEventListener('scroll', handleScroll);\n}\n\n/**\n * This code creates an IntersectionObserver object that monitors the intersection between\n * the root element (tabCategoryRef) and its targets (the child elements of listRef),\n * and updates the value of the fixedLabel variable accordingly.\n */\nfunction setTabLabelObserver () {\n /**\n * The code extracts the target element and its index from the IntersectionObserverEntry object,\n * and checks whether the target intersects with the root and is positioned above or below it.\n */\n tabLabelObserver.value = new IntersectionObserver(async (entries) => {\n // eslint-disable-next-line complexity\n entries.forEach(entry => {\n const { target } = entry;\n const index = parseInt(target.dataset.index);\n\n /**\n * If the target is positioned above the root,\n * the code updates the value of the fixed label to the label of the previous tab,\n * or the first tab if the current tab is the first one. If the target is positioned below the root, the code\n * updates the value of the fixed label to the label of the current tab.\n * If the target stops intersecting with the root and its index is 1 (the second tab),\n * the code updates the value of the fixed label to the label of the first tab.\n * NOTES:\n * This last condition is needed because sometimes it is\n * not detect the intersection between the root and the target.\n * We also provide a 50 pixels offset to the root element in the first condition to always get the\n * first tab if it has fewer emojis, because in some cases if you quickly scroll the observer does not detect it.\n */\n if (entry.isIntersecting && target.offsetTop <= tabCategoryRef.value.offsetTop + 50) {\n fixedLabel.value = tabLabels.value[index - 1]?.label ?? tabLabels.value[0]?.label;\n emits('scroll-into-tab', index - 1);\n } else if (entry.boundingClientRect.bottom <= tabCategoryRef.value?.getBoundingClientRect().bottom) {\n emits('scroll-into-tab', index);\n fixedLabel.value = tabLabels.value[index]?.label;\n } else if (index === 1) {\n emits('scroll-into-tab', index);\n fixedLabel.value = tabLabels.value[0]?.label;\n }\n });\n });\n\n /**\n * The tabLabelObserver is set to observe the root element and all its children elements with\n * the IntersectionObserver object, and sets their data-index attribute to their index.\n */\n tabLabelObserver.value.observe(tabCategoryRef.value);\n\n Array.from(listRef.value.children).forEach((child, index) => {\n tabLabelObserver.value.observe(child);\n child.dataset.index = index;\n });\n}\n\nconst handleKeyDownFilteredEmojis = (event, indexEmoji, emoji) => {\n event.preventDefault();\n\n if (Object.values(ARROW_KEYS).includes(event.key)) {\n handleArrowNavigationFiltered(event.key, indexEmoji);\n return;\n }\n\n switch (event.key) {\n case 'Tab':\n emits('focus-skin-selector');\n break;\n case 'Enter':\n selectEmoji(emoji, event);\n break;\n default:\n break;\n }\n};\n\n \nconst handleKeyDown = (event, indexTab, indexEmoji, emoji) => {\n event.preventDefault();\n\n if (Object.values(ARROW_KEYS).includes(event.key)) {\n handleArrowNavigation(event.key, indexTab, indexEmoji);\n return;\n }\n\n switch (event.key) {\n case 'Tab':\n if (event.shiftKey) {\n if (focusEmoji(indexTab, 0) && indexTab > 0) {\n scrollToTab(indexTab, true);\n } else {\n scrollToTab(1, false);\n emits('focus-search-input');\n }\n } else {\n if (focusEmoji(indexTab + 1, 0)) {\n scrollToTab(indexTab + 1 + 1, false);\n } else {\n // We are on the last emoji tabset, jump to the skin selector\n emits('focus-skin-selector');\n }\n }\n break;\n\n case 'Enter':\n selectEmoji(emoji, event);\n break;\n\n default:\n break;\n }\n};\n\nfunction selectEmoji (emoji, event) {\n emits('selected-emoji', { ...emoji, shift_key: event.shiftKey });\n}\n\nfunction highlightEmoji (emoji) {\n emits('highlighted-emoji', emoji);\n}\n\nfunction focusEmojiSelector () {\n focusEmoji(0, 0);\n}\n\nfunction focusLastEmoji () {\n scrollToTab(tabs.value.length, true);\n}\n\nonMounted(() => {\n setTabLabelObserver();\n setBottomScrollListener();\n});\n\nonBeforeUnmount(() => {\n tabLabelObserver.value.disconnect();\n listRef.value.removeEventListener('scroll', handleScroll);\n});\n\ndefineExpose({\n focusEmojiSelector,\n focusLastEmoji,\n});\n</script>\n","<template>\n <div data-qa=\"skin-selector\">\n <div\n v-show=\"isOpen\"\n class=\"d-emoji-picker__skin-list\"\n >\n <button\n v-for=\"(skin, index) in skinList\"\n :ref=\"el => { if (el) setSkinsRef(el) }\"\n :key=\"skin.name\"\n :class=\"{\n 'selected': skinSelected.skinCode === skin.skinCode,\n }\"\n @keydown=\"event => handleKeyDown(event, skin, index)\"\n @click=\"selectSkin(skin)\"\n >\n <img\n class=\"d-icon d-icon--size-500\"\n :alt=\"skin.name\"\n :aria-label=\"skin.name\"\n :title=\"skin.name\"\n :src=\"`${CDN_URL + skin.unicode_output}.png`\"\n >\n </button>\n </div>\n <div\n v-show=\"!isOpen\"\n class=\"d-emoji-picker__skin-selected\"\n >\n <dt-tooltip placement=\"top-end\">\n {{ skinSelectorButtonTooltipLabel }}\n <template #anchor>\n <button\n ref=\"skinSelectorRef\"\n :aria-label=\"skinSelectorButtonTooltipLabel\"\n tabindex=\"-1\"\n @click=\"toggleSkinList\"\n @keydown=\"event => handleKeyDown(event)\"\n >\n <img\n class=\"d-icon d-icon--size-500\"\n :alt=\"skinSelected.name\"\n :aria-label=\"skinSelected.name\"\n :title=\"skinSelected.name\"\n :src=\"`${CDN_URL + skinSelected.unicode_output}.png`\"\n >\n </button>\n </template>\n </dt-tooltip>\n </div>\n </div>\n</template>\n\n<script setup>\nimport { computed, nextTick, ref, watchEffect } from 'vue';\nimport { CDN_URL, EMOJI_PICKER_SKIN_TONE_MODIFIERS } from '@/components/emoji_picker/emoji_picker_constants.js';\nimport { DtTooltip } from '@/components/tooltip';\n\nconst props = defineProps({\n /**\n * The skin tone to apply to the emoji list\n * @type {String}\n * @required\n */\n skinTone: {\n type: String,\n required: true,\n },\n\n isHovering: {\n type: Boolean,\n default: false,\n },\n\n skinSelectorButtonTooltipLabel: {\n type: String,\n required: true,\n },\n});\n\nconst emits = defineEmits([\n /**\n * The skin tone that was selected\n * @event skin-tone\n * @type {Number}\n */\n 'skin-tone',\n 'focus-tabset',\n 'focus-last-emoji',\n]);\n\nconst skinList = [\n {\n name: ':wave_tone1:',\n unicode_output: '1f44b-1f3fb',\n skinTone: EMOJI_PICKER_SKIN_TONE_MODIFIERS.LIGHT,\n skinCode: '_tone1',\n },\n {\n name: ':wave_tone2:',\n unicode_output: '1f44b-1f3fc',\n skinTone: EMOJI_PICKER_SKIN_TONE_MODIFIERS.MEDIUM_LIGHT,\n skinCode: '_tone2',\n },\n {\n name: ':wave_tone3:',\n unicode_output: '1f44b-1f3fd',\n skinTone: EMOJI_PICKER_SKIN_TONE_MODIFIERS.MEDIUM,\n skinCode: '_tone3',\n },\n {\n name: ':wave_tone4:',\n unicode_output: '1f44b-1f3fe',\n skinTone: EMOJI_PICKER_SKIN_TONE_MODIFIERS.MEDIUM_DARK,\n skinCode: '_tone4',\n },\n {\n name: ':wave_tone5:',\n unicode_output: '1f44b-1f3ff',\n skinTone: EMOJI_PICKER_SKIN_TONE_MODIFIERS.DARK,\n skinCode: '_tone5',\n },\n {\n name: ':wave:',\n unicode_output: '1f44b',\n skinTone: EMOJI_PICKER_SKIN_TONE_MODIFIERS.DEFAULT,\n skinCode: '',\n },\n];\n\nconst isOpen = ref(false);\n\nconst skinSelectorRef = ref(null);\n\nconst skinsRef = ref([]);\n\n/**\n * It will close the skin selector if the user is hovering over the emoji list\n */\nwatchEffect(\n () => props.isHovering && (isOpen.value = false),\n);\n\n/**\n * It will initially display props.skinTone. If a new skin tone is selected,\n * it will display that until props.skinTone changes.\n */\nconst skinPassedIn = computed(() => skinList.find((skin) => skin.skinTone === props.skinTone));\nconst skinSelected = ref(skinPassedIn.value);\nwatchEffect(() => skinPassedIn.value && (skinSelected.value = skinPassedIn.value));\n\nfunction setSkinsRef (ref) {\n skinsRef.value.push(ref);\n}\nfunction focusSkinSelector () {\n skinSelectorRef.value.focus();\n}\n\nfunction selectSkin (skin) {\n skinSelected.value = skin;\n isOpen.value = false;\n emits('skin-tone', skin.skinTone);\n nextTick(() => focusSkinSelector());\n}\n\nconst handleKeyDown = (event, skin, index) => {\n event.preventDefault();\n\n if (event.key === 'ArrowLeft') {\n if (index === 0) skinsRef.value[skinsRef.value.length - 1]?.focus();\n skinsRef.value[index - 1]?.focus();\n }\n\n if (event.key === 'ArrowRight') {\n skinsRef.value[index + 1]?.focus();\n }\n\n if (event.key === 'Enter') {\n if (skin) { selectSkin(skin); } else {\n toggleSkinList();\n }\n }\n\n if (event.key === 'Tab') {\n if (event.shiftKey) {\n emits('focus-last-emoji');\n } else {\n emits('focus-tabset');\n }\n }\n};\n\nfunction toggleSkinList () {\n isOpen.value = !isOpen.value;\n nextTick(() => skinsRef.value[0].focus());\n}\n\ndefineExpose({\n focusSkinSelector,\n});\n</script>\n","<template>\n <div class=\"d-emoji-picker__data\">\n <img\n v-if=\"emoji\"\n class=\"d-icon d-icon--size-500\"\n :alt=\"emoji.name\"\n :aria-label=\"emoji.name\"\n :title=\"emoji.name\"\n :src=\"getImgSrc(emoji)\"\n >\n <div>{{ emoji?.name }}</div>\n </div>\n</template>\n\n<script setup>\nimport { CDN_URL } from '@/components/emoji_picker/emoji_picker_constants';\n\ndefineProps({\n /**\n * Emoji data\n * @type {Object}\n * @default null\n */\n emoji: {\n type: Object,\n default: null,\n },\n});\n\nfunction getImgSrc (emoji) {\n if (emoji.date_added) { // if custom emoji\n return emoji.image;\n } else { // if regular emoji\n return `${CDN_URL + emoji.unicode_character}.png`;\n }\n}\n</script>\n","<template>\n <div\n class=\"d-emoji-picker\"\n >\n <div class=\"d-emoji-picker--header\">\n <emoji-tabset\n ref=\"tabsetRef\"\n :emoji-filter=\"internalSearchQuery\"\n :show-custom-emojis-tab=\"showCustomEmojisTab\"\n :show-recently-used-tab=\"showRecentlyUsedTab\"\n :scroll-into-tab=\"scrollIntoTab\"\n :tab-set-labels=\"tabSetLabels\"\n @focus-skin-selector=\"$refs.skinSelectorRef.focusSkinSelector()\"\n @focus-search-input=\"showSearch\n ? $refs.searchInputRef.focusSearchInput()\n : $refs.emojiSelectorRef.focusEmojiSelector()\"\n @selected-tabset=\"scrollToSelectedTabset\"\n @keydown.esc=\"emits('close')\"\n />\n </div>\n <div class=\"d-emoji-picker--body\">\n <emoji-search\n v-if=\"showSearch\"\n ref=\"searchInputRef\"\n v-model=\"internalSearchQuery\"\n :search-placeholder-label=\"searchPlaceholderLabel\"\n @select-first-emoji=\"emits('selected-emoji', highlightedEmoji)\"\n @focus-tabset=\"$refs.tabsetRef.focusTabset()\"\n @focus-emoji-selector=\"$refs.emojiSelectorRef.focusEmojiSelector()\"\n @keydown.esc=\"emits('close')\"\n />\n <emoji-selector\n ref=\"emojiSelectorRef\"\n :emoji-filter=\"internalSearchQuery\"\n :skin-tone=\"skinTone\"\n :tabset-labels=\"tabSetLabels\"\n :search-results-label=\"searchResultsLabel\"\n :search-no-results-label=\"searchNoResultsLabel\"\n :recently-used-emojis=\"recentlyUsedEmojis\"\n :custom-emojis=\"customEmojis\"\n :selected-tabset=\"selectedTabset\"\n @scroll-into-tab=\"updateScrollIntoTab\"\n @highlighted-emoji=\"updateHighlightedEmoji\"\n @selected-emoji=\"emits('selected-emoji', $event)\"\n @focus-skin-selector=\"$refs.skinSelectorRef.focusSkinSelector()\"\n @focus-search-input=\"showSearch ? $refs.searchInputRef.focusSearchInput() : $refs.tabsetRef.focusTabset()\"\n @keydown.esc=\"emits('close')\"\n @scroll-bottom-reached=\"emits('scroll-bottom-reached')\"\n />\n </div>\n <div class=\"d-emoji-picker--footer\">\n <dt-button\n v-if=\"showAddEmojiButton && !highlightedEmoji\"\n importance=\"outlined\"\n :aria-label=\"addEmojiLabel\"\n class=\"d-emoji-picker__add-emoji\"\n @click=\"emits('add-emoji')\"\n >\n {{ addEmojiLabel }}\n </dt-button>\n <emoji-description :emoji=\"highlightedEmoji\" />\n <emoji-skin-selector\n ref=\"skinSelectorRef\"\n :is-hovering=\"!!highlightedEmoji\"\n :skin-selector-button-tooltip-label=\"skinSelectorButtonTooltipLabel\"\n :skin-tone=\"skinTone\"\n @skin-tone=\"emits('skin-tone', $event)\"\n @focus-tabset=\"$refs.tabsetRef.focusTabset()\"\n @focus-last-emoji=\"$refs.emojiSelectorRef.focusLastEmoji()\"\n @keydown.esc=\"emits('close')\"\n />\n </div>\n </div>\n</template>\n\n<script setup>\nimport EmojiSearch from './modules/emoji_search.vue';\nimport EmojiTabset from './modules/emoji_tabset.vue';\nimport EmojiSelector from './modules/emoji_selector.vue';\nimport EmojiSkinSelector from './modules/emoji_skin_selector.vue';\nimport EmojiDescription from './modules/emoji_description.vue';\nimport { DtButton } from '../button';\nimport { computed, ref, watch } from 'vue';\nimport { DialtoneLocalization } from '@/localization';\n\nconst props = defineProps({\n /**\n * The array with recently used emoji object\n * This list is necessary to fill the recently used tab\n * @type {Array}\n * @default []\n * @example\n * <dt-emoji-picker :recentlyUsedEmojis=\"[emojiObject, emojiObject]\" />\n */\n // TODO try to simplify this to achieve an array of unicode characters and not an entire emoji data object\n recentlyUsedEmojis: {\n type: Array,\n default: () => [],\n },\n\n /**\n * The array with custom emojis object\n * This list is necessary to fill the custom tab\n * @type {Array}\n * @default []\n * @example\n * <dt-emoji-picker :customEmojis=\"[emojiObject, emojiObject]\" />\n */\n customEmojis: {\n type: Array,\n },\n\n /**\n * The skin tone to show the emojis\n * This prop gives the possibility to use the skin tone selected by the user previously\n * @type {String}\n * @default 'Default'\n * @values 'Default', 'Light', 'MediumLight', 'Medium', 'MediumDark', 'Dark'\n * @example\n * <dt-emoji-picker :skinTone=\"'Default'\" />\n */\n skinTone: {\n type: String,\n default: 'Default',\n },\n\n /**\n\n * Sets the search query that filters emojis.\n * @type {String}\n * @example\n * <dt-emoji-picker search-query=\"smile\" />\n */\n searchQuery: {\n type: String,\n default: '',\n },\n\n /**\n * Shows the search input\n * @type {Boolean}\n * @example\n * <dt-emoji-picker :show-search=\"false\" />\n */\n showSearch: {\n type: Boolean,\n default: true,\n },\n\n /**\n * Shows the add emoji button in the footer when no emoji is highlighted\n * @type {Boolean}\n * @example\n * <dt-emoji-picker :show-add-emoji-button=\"true\" />\n */\n showAddEmojiButton: {\n type: Boolean,\n default: false,\n },\n});\n\nconst emits = defineEmits(\n [\n /**\n * It will emit the selected emoji\n * @event selected-emoji\n * @param {Object} emoji - The selected emoji from the emoji selector\n */\n 'selected-emoji',\n\n /**\n * Emitted when the user reach bottom scroll\n * This is being handled by handleScroll method\n * @event scroll-bottom-reached\n */\n 'scroll-bottom-reached',\n\n /**\n * It will emit the selected skin tone\n * @event skin-tone\n * @param {String} skin - The selected skin tone from the skin selector\n */\n 'skin-tone',\n\n /**\n * Since the keyboard events are encapsulated, we emit this event to close the picker\n * @event close\n */\n 'close',\n\n /**\n * Emitted when the user clicks on the add emoji button\n * @event add-emoji\n */\n 'add-emoji',\n ],\n);\n\nconst internalSearchQuery = ref(props.searchQuery.value);\nconst highlightedEmoji = ref(null);\nconst selectedTabset = ref({});\n\nconst scrollIntoTab = ref(0);\n\nconst showRecentlyUsedTab = computed(() => props.recentlyUsedEmojis?.length > 0);\nconst showCustomEmojisTab = computed(() => props.customEmojis?.length > 0);\n\nconst i18n = new DialtoneLocalization();\n\nconst tabSetLabels = [\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_RECENTLY_USED_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_SMILEYS_AND_PEOPLE_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_NATURE_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_FOOD_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_ACTIVITY_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_TRAVEL_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_OBJECTS_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_SYMBOLS_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_FLAGS_LABEL'),\n i18n.$t('DIALTONE_EMOJI_PICKER_TABSET_CUSTOM_LABEL'),\n];\n\nconst searchPlaceholderLabel = i18n.$t('DIALTONE_EMOJI_PICKER_SEARCH_PLACEHOLDER_LABEL');\nconst searchResultsLabel = i18n.$t('DIALTONE_EMOJI_PICKER_SEARCH_RESULTS_LABEL');\nconst searchNoResultsLabel = i18n.$t('DIALTONE_EMOJI_PICKER_SEARCH_NO_RESULTS_LABEL');\nconst skinSelectorButtonTooltipLabel = i18n.$t('DIALTONE_EMOJI_PICKER_SKIN_SELECTOR_BUTTON_TOOLTIP_LABEL');\nconst addEmojiLabel = i18n.$t('DIALTONE_EMOJI_PICKER_ADD_EMOJI_LABEL');\n\nwatch(\n () => props.searchQuery,\n (newValue) => {\n internalSearchQuery.value = newValue;\n },\n);\n\n/**\n * Handle the selected tabset event\n * We're creating a new object with the same value as selectedTabset and assigning it back to selectedTabset.\n * Vue will see this as a new object and trigger the watcher in the child component.\n * Using this method, we are able to trigger the watcher in the child component even if the value being passed is the\n * same as the previous value.\n * @event selectedTabset\n * @param tabId {String} - The id of the tab that was selected\n */\nfunction scrollToSelectedTabset (tabId) {\n internalSearchQuery.value = '';\n selectedTabset.value = { ...selectedTabset.value, tabId };\n}\n\nfunction updateScrollIntoTab (value) {\n scrollIntoTab.value = value;\n}\n\nfunction updateHighlightedEmoji (emoji) {\n highlightedEmoji.value = emoji;\n}\n</script>\n"],"mappings":"8uBAyDA,IAAM,EAAQ,EAER,GAAA,EAAA,EAAA,KAAkB,KAAK,CAE7B,SAAS,GAAe,CACtB,EAAM,oBAAqB,GAAG,CAC9B,GAAkB,CAGpB,SAAS,GAAoB,CAC3B,EAAY,MAAM,OAAO,QAE3B,EAAA,EAAA,eAAgB,CACd,GAAkB,EAClB,CAEF,EAAa,CACX,mBACD,CAAC,oDAtCM,MApCN,EAoCM,EAAA,EAAA,EAAA,cAAA,EAAA,EAAA,OADO,EAAA,QAAA,CAAA,CAjCT,GAAG,sBACC,cAAJ,IAAI,EACH,YAAa,EAAA,uBACb,cAAa,EAAA,WACb,sBAAkB,EAAA,KAAA,EAAA,GAAA,GAAEA,EAAAA,MAAK,oBAAsB,EAAM,EACrD,UAAO,+BAAKA,EAAAA,MAAK,eAAA,CAAA,CAAA,KAAA,CAAA,oDACKA,EAAAA,MAAK,uBAAA,CAAA,CAAA,UAAA,CAAA,CAAA,CAAA,OAAA,CAAA,gCACZA,EAAAA,MAAK,qBAAA,CAAA,CAAA,QAAA,CAAA,uBAEV,UAAA,EAAA,EAAA,aAGP,EAAA,EAAA,EAAA,cAAA,EAAA,EAAA,OAAA,EAAA,aAAA,CAAA,CADA,KAAK,MAAK,CAAA,CAAA,CAAA,OAIN,EAAA,WAAW,OAAM,EAAA,MACtB,iCAeW,EAAA,EAAA,EAAA,cAAA,EAAA,EAAA,OAAA,EAAA,QAAA,CAAA,CAZV,WAAW,QACX,KAAK,KACL,MAAM,kCACN,OAAA,GACA,KAAK,QACJ,QAAO,IAEG,MAAA,EAAA,EAAA,aAGP,EAAA,EAAA,EAAA,cAAA,EAAA,EAAA,OAAA,EAAA,YAAA,CAAA,CADA,KAAK,MAAK,CAAA,CAAA,CAAA,ubCgBxB,IAAM,EAAQ,EA0CR,EAAQ,EAYR,EAAY,CAChB,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,YAAa,CACnD,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,gBAAiB,CACvD,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,kBAAmB,CACzD,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,WAAY,CAClD,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,aAAc,CACpD,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,qBAAsB,CAC5D,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,gBAAiB,CACvD,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,YAAa,CACnD,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,WAAY,CAClD,CAAE,MAAO,EAAM,aAAa,GAAI,KAAM,EAAA,kBAAmB,CAC1D,CAEK,GAAA,EAAA,EAAA,cAAsB,CAC1B,IAAM,EAAW,EAAM,oBAAsB,EAAY,EAAU,MAAM,EAAE,CAM3E,OAJK,EAAM,qBACT,EAAS,KAAK,CAGT,EAAS,KAAK,EAAK,KAAW,CACnC,GAAG,EAEH,IAAK,EAAQ,GAAG,UAAU,CAC1B,SAAU,EAAQ,GAAG,UAAU,CAChC,EAAE,EACH,CAEI,GAAA,EAAA,EAAA,cAA6B,EAAM,YAAY,OAAS,EAAE,CAE1D,GAAA,EAAA,EAAA,KAAkB,IAAI,CAEtB,GAAA,EAAA,EAAA,KAAgB,EAAE,CAAC,EAEzB,EAAA,EAAA,WAAY,EAAM,kBACV,CACC,EAAY,QACf,EAAY,OAAS,EAAM,cAAgB,GAAG,UAAU,GAE1D,EAEJ,EAAA,EAAA,OAAM,MACE,CACA,EAAY,QACd,EAAY,MAAQ,OAEtB,CAOJ,SAAS,EAAc,EAAI,CAEzB,IAAM,EAAU,SAAS,EAAG,CAE5B,EAAY,MAAQ,EACpB,EAAM,kBAAmB,EAAQ,CAGnC,SAAS,EAAc,EAAK,CAG1B,EAAU,MAAM,KAAK,EAAA,cAAc,EAAI,IAAI,CAAC,CAG9C,SAAS,GAAe,CACtB,EAAU,MAAM,GAAG,OAAO,CAG5B,SAAS,EAAe,EAAO,EAAO,CAChC,EAAM,MAAQ,UAChB,EAAa,EAAM,CAEnB,EAAU,MAAM,EAAQ,GAAG,MAAM,EAG/B,EAAM,MAAQ,QAChB,EAAM,gBAAgB,CAClB,EAAM,SACR,EAAM,sBAAsB,CAE5B,EAAM,qBAAqB,EAI3B,EAAM,MAAQ,aAEhB,EAAM,qBAAqB,QAI/B,EAAa,CACX,cACD,CAAC,oDAzKM,MA1BN,EA0BM,EAAA,EAAA,EAAA,cAAA,EAAA,EAAA,OADW,EAAA,QAAA,CAAA,CAvBZ,SAAU,EAAA,MACX,KAAK,KACL,iBAAe,gCAEJ,MAAA,EAAA,EAAA,aAEqB,GAAA,EAAA,EAAA,WAAA,GAAA,EAAA,EAAA,EAAA,oBAerB,EAAA,SAAA,MAAA,EAAA,EAAA,YAfgB,EAAA,OAAf,EAAK,qDAeN,EAAA,QAAA,CAAA,CAdN,GAAI,EAAI,GACR,IAAK,EAAI,cACT,IAAK,GAAE,CAAU,GAAI,EAAa,EAAE,EACpC,MAAO,EAAI,MACX,WAAU,EAAI,QACd,SAAU,EAAK,EAChB,gBAAc,sBACb,UAAO,GAAE,EAAc,EAAQ,EAAI,GAAE,uCACjB,EAAa,EAAI,GAAE,CAAA,CAAA,OAAA,CAAA,6BAKtC,GAAA,EAAA,EAAA,YAAA,EAAA,EAAA,EAAA,cAAA,EAAA,EAAA,yBAFK,EAAI,KAAI,CAAA,CACb,KAAK,MAAK,CAAA,EAAA,CAAA,kHCnBtB,SAAgB,GAAyB,CACvC,IAAM,GAAA,EAAA,EAAA,KAAgB,EAAE,CAAC,CACnB,GAAA,EAAA,EAAA,KAAwB,EAAE,CAAC,CAC3B,GAAA,EAAA,EAAA,KAAkB,GAAM,CACxB,GAAA,EAAA,EAAA,KAAsB,GAAK,CAEjC,SAAS,EAAkB,EAAU,EAAY,CAC1C,EAAW,EAAU,EAAa,EAAE,GACnC,EAAU,MAAM,EAAW,GAC7B,EAAW,EAAW,EAAG,EAAU,MAAM,EAAW,GAAG,OAAS,EAAE,CAElE,EAAW,EAAU,MAAM,OAAS,EAAG,EAAU,MAAM,EAAU,MAAM,OAAS,GAAG,OAAS,EAAE,EAKpG,SAAS,EAAmB,EAAU,EAAY,CAC3C,EAAW,EAAU,EAAa,EAAE,EAClC,EAAW,EAAW,EAAG,EAAE,EAC9B,EAAW,EAAG,EAAE,CAKtB,SAAS,EAA0B,EAAU,EAAY,CAClD,EAAW,EAAG,EAAa,EAAE,EAChC,EAAW,EAAG,EAAkB,MAAM,OAAS,EAAE,CAIrD,SAAS,EAA2B,EAAU,EAAY,CACnD,EAAW,EAAG,EAAa,EAAE,EAChC,EAAW,EAAG,EAAE,CAIpB,SAAS,EAA6B,EAAW,EAAU,EAAY,CACjE,EAAY,MACV,IAAc,OAChB,EAAyB,EAAU,EAAW,CACrC,IAAc,SACvB,EAA0B,EAAU,EAAW,CAG7C,IAAc,OAChB,EAAiB,EAAU,EAAW,CAC7B,IAAc,SACvB,EAAkB,EAAU,EAAW,CAK7C,SAAS,EAAY,EAAU,EAAY,CACzC,IAAM,EAAW,EAAY,MACzB,EAAkB,QAAQ,GAC1B,EAAU,QAAQ,KAAY,GAOlC,OALI,GACF,EAAS,OAAO,CACT,IAGF,GAGT,SAAS,EAAa,EAAI,EAAU,EAAY,CACzC,EAAU,MAAM,KACnB,EAAU,MAAM,GAAY,EAAE,EAEhC,EAAU,MAAM,GAAU,GAAc,EAG1C,SAAS,EAAgB,EAAI,EAAO,CAClC,EAAkB,MAAM,GAAS,EAGnC,SAAS,EAA+B,EAAK,EAAY,CAGvD,GAFA,EAAgB,MAAQ,GAEpB,IAAQC,EAAAA,WAAW,SAAU,CAC/B,IAAM,EAAW,EAAA,EAEjB,GAAI,CAAC,EAAW,EAAG,EAAA,EAA4B,CAAE,CAC/C,IAAM,EACN,EAAkB,MAAM,OAAU,EAAkB,MAAM,OAAA,EAA2B,E