mirror of
https://github.com/open-webui/open-webui
synced 2025-01-19 09:16:44 +00:00
refac: emoji picker optimisation
This commit is contained in:
parent
cce8f37ada
commit
6ecee8b920
7
package-lock.json
generated
7
package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"@mediapipe/tasks-vision": "^0.10.17",
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^2.0.0",
|
||||
"@sveltejs/svelte-virtual-list": "^3.0.1",
|
||||
"@tiptap/core": "^2.10.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.10.0",
|
||||
"@tiptap/extension-highlight": "^2.10.0",
|
||||
@ -2291,6 +2292,12 @@
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz",
|
||||
|
@ -57,6 +57,7 @@
|
||||
"@mediapipe/tasks-vision": "^0.10.17",
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^2.0.0",
|
||||
"@sveltejs/svelte-virtual-list": "^3.0.1",
|
||||
"@tiptap/core": "^2.10.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.10.0",
|
||||
"@tiptap/extension-highlight": "^2.10.0",
|
||||
|
@ -1,63 +1,97 @@
|
||||
<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 = {};
|
||||
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;
|
||||
}
|
||||
// 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 {
|
||||
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>
|
||||
|
||||
<!-- TODO: Rendering Optimisation, This works but it's slow af -->
|
||||
|
||||
<DropdownMenu.Root
|
||||
bind:open={show}
|
||||
closeFocus={false}
|
||||
@ -72,75 +106,61 @@
|
||||
<DropdownMenu.Trigger>
|
||||
<slot />
|
||||
</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>
|
||||
|
Loading…
Reference in New Issue
Block a user