2024-12-30 10:20:09 +00:00
|
|
|
<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';
|
2024-12-31 11:24:05 +00:00
|
|
|
import VirtualList from '@sveltejs/svelte-virtual-list';
|
2024-12-30 10:20:09 +00:00
|
|
|
|
|
|
|
|
export let onClose = () => {};
|
2024-12-31 07:06:34 +00:00
|
|
|
export let onSubmit = (name) => {};
|
2024-12-30 10:20:09 +00:00
|
|
|
export let side = 'top';
|
|
|
|
|
export let align = 'start';
|
|
|
|
|
export let user = null;
|
|
|
|
|
|
2024-12-31 11:24:05 +00:00
|
|
|
let show = false;
|
|
|
|
|
let emojis = emojiShortCodes;
|
2024-12-30 10:20:09 +00:00
|
|
|
let search = '';
|
2024-12-31 11:24:05 +00:00
|
|
|
let flattenedEmojis = [];
|
|
|
|
|
let emojiRows = [];
|
2024-12-30 10:20:09 +00:00
|
|
|
|
2024-12-31 11:24:05 +00:00
|
|
|
// Reactive statement to filter the emojis based on search query
|
|
|
|
|
$: {
|
|
|
|
|
if (search) {
|
|
|
|
|
emojis = Object.keys(emojiShortCodes).reduce((acc, key) => {
|
|
|
|
|
if (key.includes(search)) {
|
|
|
|
|
acc[key] = emojiShortCodes[key];
|
2024-12-30 10:20:09 +00:00
|
|
|
} else {
|
2024-12-31 11:24:05 +00:00
|
|
|
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];
|
|
|
|
|
}
|
2024-12-30 10:20:09 +00:00
|
|
|
}
|
|
|
|
|
}
|
2024-12-31 11:24:05 +00:00
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
} else {
|
|
|
|
|
emojis = emojiShortCodes;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Flatten emoji groups and group them into rows of 8 for virtual scrolling
|
|
|
|
|
$: {
|
|
|
|
|
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]
|
|
|
|
|
}))
|
|
|
|
|
);
|
2024-12-30 10:20:09 +00:00
|
|
|
}
|
2024-12-31 11:24:05 +00:00
|
|
|
});
|
2025-01-03 04:23:07 +00:00
|
|
|
// Group emojis into rows of 8
|
2024-12-31 11:24:05 +00:00
|
|
|
emojiRows = [];
|
|
|
|
|
let currentRow = [];
|
|
|
|
|
flattenedEmojis.forEach((item) => {
|
|
|
|
|
if (item.type === 'emoji') {
|
|
|
|
|
currentRow.push(item);
|
2025-01-03 04:23:07 +00:00
|
|
|
if (currentRow.length === 8) {
|
2024-12-31 11:24:05 +00:00
|
|
|
emojiRows.push(currentRow);
|
|
|
|
|
currentRow = [];
|
|
|
|
|
}
|
|
|
|
|
} else if (item.type === 'group') {
|
|
|
|
|
if (currentRow.length > 0) {
|
|
|
|
|
emojiRows.push(currentRow); // Push the remaining row
|
|
|
|
|
currentRow = [];
|
|
|
|
|
}
|
|
|
|
|
emojiRows.push([item]); // Add the group label as a separate row
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (currentRow.length > 0) {
|
|
|
|
|
emojiRows.push(currentRow); // Push the final row
|
|
|
|
|
}
|
2024-12-30 10:20:09 +00:00
|
|
|
}
|
2024-12-31 11:24:05 +00:00
|
|
|
const ROW_HEIGHT = 48; // Approximate height for a row with multiple emojis
|
|
|
|
|
// Handle emoji selection
|
|
|
|
|
function selectEmoji(emoji) {
|
|
|
|
|
const selectedCode = emoji.shortCodes[0];
|
|
|
|
|
onSubmit(selectedCode);
|
|
|
|
|
show = false;
|
2024-12-31 08:11:46 +00:00
|
|
|
}
|
2024-12-30 10:20:09 +00:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<DropdownMenu.Root
|
|
|
|
|
bind:open={show}
|
|
|
|
|
closeFocus={false}
|
|
|
|
|
onOpenChange={(state) => {
|
|
|
|
|
if (!state) {
|
|
|
|
|
search = '';
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
typeahead={false}
|
|
|
|
|
>
|
|
|
|
|
<DropdownMenu.Trigger>
|
|
|
|
|
<slot />
|
|
|
|
|
</DropdownMenu.Trigger>
|
2024-12-31 11:24:05 +00:00
|
|
|
<DropdownMenu.Content
|
2025-02-16 03:27:25 +00:00
|
|
|
class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-9999 shadow-lg dark:text-white"
|
2024-12-31 11:24:05 +00:00
|
|
|
sideOffset={8}
|
|
|
|
|
{side}
|
|
|
|
|
{align}
|
|
|
|
|
transition={flyAndScale}
|
|
|
|
|
>
|
|
|
|
|
<div class="mb-1 px-3 pt-2 pb-2">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
2025-02-16 03:27:25 +00:00
|
|
|
class="w-full text-sm bg-transparent outline-hidden"
|
2024-12-31 11:24:05 +00:00
|
|
|
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}
|
2025-01-03 04:23:07 +00:00
|
|
|
<div class="w-full flex ml-0.5">
|
2024-12-31 11:24:05 +00:00
|
|
|
<VirtualList rowHeight={ROW_HEIGHT} items={emojiRows} height={384} let:item>
|
|
|
|
|
<div class="w-full">
|
|
|
|
|
{#if item.length === 1 && item[0].type === 'group'}
|
|
|
|
|
<!-- Render group header -->
|
|
|
|
|
<div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400">
|
|
|
|
|
{item[0].label}
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<!-- Render emojis in a row -->
|
2025-01-03 04:23:07 +00:00
|
|
|
<div class="flex items-center gap-1.5 w-full">
|
2024-12-31 11:24:05 +00:00
|
|
|
{#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>
|
2024-12-30 10:20:09 +00:00
|
|
|
</DropdownMenu.Root>
|