|
@@ -1,63 +1,97 @@
|
|
<script lang="ts">
|
|
<script lang="ts">
|
|
import { DropdownMenu } from 'bits-ui';
|
|
import { DropdownMenu } from 'bits-ui';
|
|
import { flyAndScale } from '$lib/utils/transitions';
|
|
import { flyAndScale } from '$lib/utils/transitions';
|
|
-
|
|
|
|
import emojiGroups from '$lib/emoji-groups.json';
|
|
import emojiGroups from '$lib/emoji-groups.json';
|
|
import emojiShortCodes from '$lib/emoji-shortcodes.json';
|
|
import emojiShortCodes from '$lib/emoji-shortcodes.json';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
|
|
+ import VirtualList from '@sveltejs/svelte-virtual-list';
|
|
|
|
|
|
export let onClose = () => {};
|
|
export let onClose = () => {};
|
|
export let onSubmit = (name) => {};
|
|
export let onSubmit = (name) => {};
|
|
export let side = 'top';
|
|
export let side = 'top';
|
|
export let align = 'start';
|
|
export let align = 'start';
|
|
-
|
|
|
|
export let user = null;
|
|
export let user = null;
|
|
- let show = false;
|
|
|
|
|
|
|
|
- let emojis = {};
|
|
|
|
|
|
+ let show = false;
|
|
|
|
+ let emojis = emojiShortCodes;
|
|
let search = '';
|
|
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;
|
|
|
|
- }
|
|
|
|
|
|
+ // 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];
|
|
} else {
|
|
} else {
|
|
- if (emojiShortCodes[key].includes(search)) {
|
|
|
|
- acc[key] = emojiShortCodes[key];
|
|
|
|
|
|
+ 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;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ // 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]
|
|
|
|
+ }))
|
|
|
|
+ );
|
|
}
|
|
}
|
|
-
|
|
|
|
- return acc;
|
|
|
|
- }, {});
|
|
|
|
- } else {
|
|
|
|
- emojis = emojiShortCodes;
|
|
|
|
|
|
+ });
|
|
|
|
+ // Group emojis into rows of 6
|
|
|
|
+ emojiRows = [];
|
|
|
|
+ let currentRow = [];
|
|
|
|
+ flattenedEmojis.forEach((item) => {
|
|
|
|
+ if (item.type === 'emoji') {
|
|
|
|
+ currentRow.push(item);
|
|
|
|
+ if (currentRow.length === 7) {
|
|
|
|
+ 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
|
|
|
|
+ }
|
|
}
|
|
}
|
|
-
|
|
|
|
- $: if (show) {
|
|
|
|
- init();
|
|
|
|
- } else {
|
|
|
|
- destroy();
|
|
|
|
|
|
+ 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;
|
|
}
|
|
}
|
|
-
|
|
|
|
- const init = () => {
|
|
|
|
- emojis = emojiShortCodes;
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- const destroy = () => {
|
|
|
|
- search = '';
|
|
|
|
- emojis = {};
|
|
|
|
- };
|
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
-<!-- TODO: Rendering Optimisation, This works but it's slow af -->
|
|
|
|
-
|
|
|
|
<DropdownMenu.Root
|
|
<DropdownMenu.Root
|
|
bind:open={show}
|
|
bind:open={show}
|
|
closeFocus={false}
|
|
closeFocus={false}
|
|
@@ -72,75 +106,61 @@
|
|
<DropdownMenu.Trigger>
|
|
<DropdownMenu.Trigger>
|
|
<slot />
|
|
<slot />
|
|
</DropdownMenu.Trigger>
|
|
</DropdownMenu.Trigger>
|
|
-
|
|
|
|
- <slot name="content">
|
|
|
|
- <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>
|
|
|
|
- <div class=" w-full flex justify-start h-96 overflow-y-auto px-3 pb-3 text-sm">
|
|
|
|
- {#if show}
|
|
|
|
- <div>
|
|
|
|
- {#if Object.keys(emojis).length === 0}
|
|
|
|
- <div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div>
|
|
|
|
- {:else}
|
|
|
|
- {#each Object.keys(emojiGroups) as group (group)}
|
|
|
|
- {@const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji])}
|
|
|
|
- {#if groupEmojis.length > 0}
|
|
|
|
- <div class="flex flex-col">
|
|
|
|
- <div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400">
|
|
|
|
- {group}
|
|
|
|
- </div>
|
|
|
|
-
|
|
|
|
- <div class="flex mb-2 flex-wrap gap-1">
|
|
|
|
- {#each groupEmojis as emoji (emoji)}
|
|
|
|
- <Tooltip
|
|
|
|
- content={(typeof emojiShortCodes[emoji] === 'string'
|
|
|
|
- ? [emojiShortCodes[emoji]]
|
|
|
|
- : emojiShortCodes[emoji]
|
|
|
|
- )
|
|
|
|
- .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={() => {
|
|
|
|
- typeof emojiShortCodes[emoji] === 'string'
|
|
|
|
- ? onSubmit(emojiShortCodes[emoji])
|
|
|
|
- : onSubmit(emojiShortCodes[emoji][0]);
|
|
|
|
-
|
|
|
|
- show = false;
|
|
|
|
- }}
|
|
|
|
- >
|
|
|
|
- <img
|
|
|
|
- src="/assets/emojis/{emoji.toLowerCase()}.svg"
|
|
|
|
- alt={emoji}
|
|
|
|
- class="size-5"
|
|
|
|
- loading="lazy"
|
|
|
|
- />
|
|
|
|
- </button>
|
|
|
|
- </Tooltip>
|
|
|
|
- {/each}
|
|
|
|
- </div>
|
|
|
|
- </div>
|
|
|
|
- {/if}
|
|
|
|
- {/each}
|
|
|
|
- {/if}
|
|
|
|
- </div>
|
|
|
|
- {/if}
|
|
|
|
- </div>
|
|
|
|
- </DropdownMenu.Content>
|
|
|
|
- </slot>
|
|
|
|
|
|
+ <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-2">
|
|
|
|
+ <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 -->
|
|
|
|
+ <div class="flex items-center gap-2 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>
|
|
</DropdownMenu.Root>
|