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,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 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
emojis = Object.keys(emojiShortCodes).reduce((acc, key) => { $: {
if (key.includes(search)) { if (search) {
acc[key] = emojiShortCodes[key]; emojis = Object.keys(emojiShortCodes).reduce((acc, key) => {
} else { if (key.includes(search)) {
if (Array.isArray(emojiShortCodes[key])) { acc[key] = emojiShortCodes[key];
const filtered = emojiShortCodes[key].filter((emoji) => emoji.includes(search));
if (filtered.length) {
acc[key] = filtered;
}
} else { } else {
if (emojiShortCodes[key].includes(search)) { if (Array.isArray(emojiShortCodes[key])) {
acc[key] = 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; // Group emojis into rows of 6
}, {}); emojiRows = [];
} else { let currentRow = [];
emojis = emojiShortCodes; 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
$: if (show) { // Handle emoji selection
init(); function selectEmoji(emoji) {
} else { const selectedCode = emoji.shortCodes[0];
destroy(); 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>
<DropdownMenu.Content
<slot name="content"> class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white"
<DropdownMenu.Content sideOffset={8}
class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white" {side}
sideOffset={8} {align}
{side} transition={flyAndScale}
{align} >
transition={flyAndScale} <div class="mb-1 px-3 pt-2 pb-2">
> <input
<div class="mb-1 px-3 pt-2 pb-2"> type="text"
<input class="w-full text-sm bg-transparent outline-none"
type="text" placeholder="Search all emojis"
class="w-full text-sm bg-transparent outline-none" bind:value={search}
placeholder="Search all emojis" />
bind:value={search} </div>
/> <!-- Virtualized Emoji List -->
</div> <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 emojiRows.length === 0}
{#if show} <div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div>
<div> {:else}
{#if Object.keys(emojis).length === 0} <div class="w-full flex ml-2">
<div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div> <VirtualList rowHeight={ROW_HEIGHT} items={emojiRows} height={384} let:item>
{:else} <div class="w-full">
{#each Object.keys(emojiGroups) as group (group)} {#if item.length === 1 && item[0].type === 'group'}
{@const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji])} <!-- Render group header -->
{#if groupEmojis.length > 0} <div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400">
<div class="flex flex-col"> {item[0].label}
<div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400"> </div>
{group} {:else}
</div> <!-- Render emojis in a row -->
<div class="flex items-center gap-2 w-full">
<div class="flex mb-2 flex-wrap gap-1"> {#each item as emojiItem}
{#each groupEmojis as emoji (emoji)} <Tooltip
<Tooltip content={emojiItem.shortCodes.map((code) => `:${code}:`).join(', ')}
content={(typeof emojiShortCodes[emoji] === 'string' placement="top"
? [emojiShortCodes[emoji]] >
: emojiShortCodes[emoji] <button
) class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition"
.map((code) => `:${code}:`) on:click={() => selectEmoji(emojiItem)}
.join(', ')} >
placement="top" <img
> src="/assets/emojis/{emojiItem.name.toLowerCase()}.svg"
<button alt={emojiItem.name}
class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition" class="size-5"
on:click={() => { loading="lazy"
typeof emojiShortCodes[emoji] === 'string' />
? onSubmit(emojiShortCodes[emoji]) </button>
: onSubmit(emojiShortCodes[emoji][0]); </Tooltip>
{/each}
show = false; </div>
}} {/if}
> </div>
<img </VirtualList>
src="/assets/emojis/{emoji.toLowerCase()}.svg" </div>
alt={emoji} {/if}
class="size-5" </div>
loading="lazy" </DropdownMenu.Content>
/>
</button>
</Tooltip>
{/each}
</div>
</div>
{/if}
{/each}
{/if}
</div>
{/if}
</div>
</DropdownMenu.Content>
</slot>
</DropdownMenu.Root> </DropdownMenu.Root>