| <script lang="ts"> |
| import { DropdownMenu } from 'bits-ui'; |
| import { flyAndScale } from '$lib/utils/transitions'; |
| import emojiGroups from '$lib/emoji-groups.json'; |
| import emojiShortCodes from '$lib/emoji-shortcodes.json'; |
| import Tooltip from '$lib/components/common/Tooltip.svelte'; |
| import VirtualList from '@sveltejs/svelte-virtual-list'; |
| |
| export let onClose = () => {}; |
| export let onSubmit = (name) => {}; |
| export let side = 'top'; |
| export let align = 'start'; |
| export let user = null; |
| |
| let show = false; |
| let emojis = emojiShortCodes; |
| let search = ''; |
| let flattenedEmojis = []; |
| let emojiRows = []; |
| |
| |
| $: { |
| if (search) { |
| emojis = Object.keys(emojiShortCodes).reduce((acc, key) => { |
| if (key.includes(search)) { |
| acc[key] = emojiShortCodes[key]; |
| } else { |
| if (Array.isArray(emojiShortCodes[key])) { |
| const filtered = emojiShortCodes[key].filter((emoji) => emoji.includes(search)); |
| if (filtered.length) { |
| acc[key] = filtered; |
| } |
| } else { |
| if (emojiShortCodes[key].includes(search)) { |
| acc[key] = emojiShortCodes[key]; |
| } |
| } |
| } |
| return acc; |
| }, {}); |
| } else { |
| emojis = emojiShortCodes; |
| } |
| } |
| |
| $: { |
| flattenedEmojis = []; |
| Object.keys(emojiGroups).forEach((group) => { |
| const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji]); |
| if (groupEmojis.length > 0) { |
| flattenedEmojis.push({ type: 'group', label: group }); |
| flattenedEmojis.push( |
| ...groupEmojis.map((emoji) => ({ |
| type: 'emoji', |
| name: emoji, |
| shortCodes: |
| typeof emojiShortCodes[emoji] === 'string' |
| ? [emojiShortCodes[emoji]] |
| : emojiShortCodes[emoji] |
| })) |
| ); |
| } |
| }); |
| |
| emojiRows = []; |
| let currentRow = []; |
| flattenedEmojis.forEach((item) => { |
| if (item.type === 'emoji') { |
| currentRow.push(item); |
| if (currentRow.length === 8) { |
| emojiRows.push(currentRow); |
| currentRow = []; |
| } |
| } else if (item.type === 'group') { |
| if (currentRow.length > 0) { |
| emojiRows.push(currentRow); // Push the remaining row |
| currentRow = []; |
| } |
| emojiRows.push([item]); |
| } |
| }); |
| if (currentRow.length > 0) { |
| emojiRows.push(currentRow); // Push the final row |
| } |
| } |
| const ROW_HEIGHT = 48; |
| |
| function selectEmoji(emoji) { |
| const selectedCode = emoji.shortCodes[0]; |
| onSubmit(selectedCode); |
| show = false; |
| } |
| </script> |
|
|
| <DropdownMenu.Root |
| bind:open={show} |
| closeFocus={false} |
| onOpenChange={(state) => { |
| if (!state) { |
| search = ''; |
| onClose(); |
| } |
| }} |
| typeahead={false} |
| > |
| <DropdownMenu.Trigger> |
| <slot /> |
| </DropdownMenu.Trigger> |
| <DropdownMenu.Content |
| class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white" |
| sideOffset={8} |
| {side} |
| {align} |
| transition={flyAndScale} |
| > |
| <div class="mb-1 px-3 pt-2 pb-2"> |
| <input |
| type="text" |
| class="w-full text-sm bg-transparent outline-none" |
| placeholder="Search all emojis" |
| bind:value={search} |
| /> |
| </div> |
| <!-- Virtualized Emoji List --> |
| <div class="w-full flex justify-start h-96 overflow-y-auto px-3 pb-3 text-sm"> |
| {#if emojiRows.length === 0} |
| <div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div> |
| {:else} |
| <div class="w-full flex ml-0.5"> |
| <VirtualList rowHeight={ROW_HEIGHT} items={emojiRows} height={384} let:item> |
| <div class="w-full"> |
| {#if item.length === 1 && item[0].type === 'group'} |
| |
| <div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400"> |
| {item[0].label} |
| </div> |
| {:else} |
| |
| <div class="flex items-center gap-1.5 w-full"> |
| {#each item as emojiItem} |
| <Tooltip |
| content={emojiItem.shortCodes.map((code) => `:${code}:`).join(', ')} |
| placement="top" |
| > |
| <button |
| class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition" |
| on:click={() => selectEmoji(emojiItem)} |
| > |
| <img |
| src="/assets/emojis/{emojiItem.name.toLowerCase()}.svg" |
| alt={emojiItem.name} |
| class="size-5" |
| loading="lazy" |
| /> |
| </button> |
| </Tooltip> |
| {/each} |
| </div> |
| {/if} |
| </div> |
| </VirtualList> |
| </div> |
| {/if} |
| </div> |
| </DropdownMenu.Content> |
| </DropdownMenu.Root> |
|
|