refac: emoji picker optimisation

This commit is contained in:
Timothy Jaeryang Baek 2024-12-31 03:24:05 -08:00
parent cce8f37ada
commit 6ecee8b920
3 changed files with 136 additions and 108 deletions

7
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@mediapipe/tasks-vision": "^0.10.17", "@mediapipe/tasks-vision": "^0.10.17",
"@pyscript/core": "^0.4.32", "@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^2.0.0", "@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@tiptap/core": "^2.10.0", "@tiptap/core": "^2.10.0",
"@tiptap/extension-code-block-lowlight": "^2.10.0", "@tiptap/extension-code-block-lowlight": "^2.10.0",
"@tiptap/extension-highlight": "^2.10.0", "@tiptap/extension-highlight": "^2.10.0",
@ -2291,6 +2292,12 @@
"vite": "^5.0.3 || ^6.0.0" "vite": "^5.0.3 || ^6.0.0"
} }
}, },
"node_modules/@sveltejs/svelte-virtual-list": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/svelte-virtual-list/-/svelte-virtual-list-3.0.1.tgz",
"integrity": "sha512-aF9TptS7NKKS7/TqpsxQBSDJ9Q0XBYzBehCeIC5DzdMEgrJZpIYao9LRLnyyo6SVodpapm2B7FE/Lj+FSA5/SQ==",
"license": "LIL"
},
"node_modules/@sveltejs/vite-plugin-svelte": { "node_modules/@sveltejs/vite-plugin-svelte": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz",

View File

@ -57,6 +57,7 @@
"@mediapipe/tasks-vision": "^0.10.17", "@mediapipe/tasks-vision": "^0.10.17",
"@pyscript/core": "^0.4.32", "@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^2.0.0", "@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@tiptap/core": "^2.10.0", "@tiptap/core": "^2.10.0",
"@tiptap/extension-code-block-lowlight": "^2.10.0", "@tiptap/extension-code-block-lowlight": "^2.10.0",
"@tiptap/extension-highlight": "^2.10.0", "@tiptap/extension-highlight": "^2.10.0",

View File

@ -1,23 +1,26 @@
<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 show = false;
let emojis = emojiShortCodes;
let emojis = {};
let search = ''; let search = '';
let flattenedEmojis = [];
let emojiRows = [];
$: if (search) { // Reactive statement to filter the emojis based on search query
$: {
if (search) {
emojis = Object.keys(emojiShortCodes).reduce((acc, key) => { emojis = Object.keys(emojiShortCodes).reduce((acc, key) => {
if (key.includes(search)) { if (key.includes(search)) {
acc[key] = emojiShortCodes[key]; acc[key] = emojiShortCodes[key];
@ -33,31 +36,62 @@
} }
} }
} }
return acc; return acc;
}, {}); }, {});
} else { } else {
emojis = emojiShortCodes; emojis = emojiShortCodes;
} }
$: if (show) {
init();
} else {
destroy();
} }
// Flatten emoji groups and group them into rows of 8 for virtual scrolling
const init = () => { $: {
emojis = emojiShortCodes; flattenedEmojis = [];
}; Object.keys(emojiGroups).forEach((group) => {
const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji]);
const destroy = () => { if (groupEmojis.length > 0) {
search = ''; flattenedEmojis.push({ type: 'group', label: group });
emojis = {}; flattenedEmojis.push(
}; ...groupEmojis.map((emoji) => ({
type: 'emoji',
name: emoji,
shortCodes:
typeof emojiShortCodes[emoji] === 'string'
? [emojiShortCodes[emoji]]
: emojiShortCodes[emoji]
}))
);
}
});
// 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
}
}
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;
}
</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,8 +106,6 @@
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<slot /> <slot />
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<slot name="content">
<DropdownMenu.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" class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white"
sideOffset={8} sideOffset={8}
@ -89,44 +121,34 @@
bind:value={search} bind:value={search}
/> />
</div> </div>
<!-- Virtualized Emoji List -->
<div class="w-full flex justify-start h-96 overflow-y-auto px-3 pb-3 text-sm"> <div class="w-full flex justify-start h-96 overflow-y-auto px-3 pb-3 text-sm">
{#if show} {#if emojiRows.length === 0}
<div>
{#if Object.keys(emojis).length === 0}
<div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div> <div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div>
{:else} {:else}
{#each Object.keys(emojiGroups) as group (group)} <div class="w-full flex ml-2">
{@const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji])} <VirtualList rowHeight={ROW_HEIGHT} items={emojiRows} height={384} let:item>
{#if groupEmojis.length > 0} <div class="w-full">
<div class="flex flex-col"> {#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"> <div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400">
{group} {item[0].label}
</div> </div>
{:else}
<div class="flex mb-2 flex-wrap gap-1"> <!-- Render emojis in a row -->
{#each groupEmojis as emoji (emoji)} <div class="flex items-center gap-2 w-full">
{#each item as emojiItem}
<Tooltip <Tooltip
content={(typeof emojiShortCodes[emoji] === 'string' content={emojiItem.shortCodes.map((code) => `:${code}:`).join(', ')}
? [emojiShortCodes[emoji]]
: emojiShortCodes[emoji]
)
.map((code) => `:${code}:`)
.join(', ')}
placement="top" placement="top"
> >
<button <button
class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition" class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition"
on:click={() => { on:click={() => selectEmoji(emojiItem)}
typeof emojiShortCodes[emoji] === 'string'
? onSubmit(emojiShortCodes[emoji])
: onSubmit(emojiShortCodes[emoji][0]);
show = false;
}}
> >
<img <img
src="/assets/emojis/{emoji.toLowerCase()}.svg" src="/assets/emojis/{emojiItem.name.toLowerCase()}.svg"
alt={emoji} alt={emojiItem.name}
class="size-5" class="size-5"
loading="lazy" loading="lazy"
/> />
@ -134,13 +156,11 @@
</Tooltip> </Tooltip>
{/each} {/each}
</div> </div>
{/if}
</div> </div>
{/if} </VirtualList>
{/each}
{/if}
</div> </div>
{/if} {/if}
</div> </div>
</DropdownMenu.Content> </DropdownMenu.Content>
</slot>
</DropdownMenu.Root> </DropdownMenu.Root>