frontend/app/components/notes/reactions/picker/index.vue
Jesse Wierzbinski 7f7cf20311
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 1s
Deploy to GitHub Pages / build (push) Failing after 1s
Deploy to GitHub Pages / deploy (push) Has been skipped
Docker / build (push) Failing after 1s
Mirror to Codeberg / Mirror (push) Failing after 1s
chore: ⬆️ Upgrade to Nuxt 4
2025-07-16 07:48:39 +02:00

139 lines
4.8 KiB
Vue

<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<slot />
</PopoverTrigger>
<PopoverContent class="p-0 w-fit">
<div class="grid-cols-[minmax(0,1fr)_auto] gap-0 grid divide-x *:h-112 *:overflow-y-auto"
orientation="vertical">
<div class="grid grid-rows-[auto_minmax(0,1fr)_auto] gap-0" ref="emojiContainer">
<div class="p-2">
<Input placeholder="Search" v-model="filter" />
</div>
<VList :data="virtualizedItems" #default="{ item }" class="relative" :style="{
width: `calc(var(--spacing) * ((12 * ${EMOJI_PER_ROW}) + (${EMOJI_PER_ROW} - 1)) + var(--spacing) * 4)`,
}">
<CategoryHeader :key="item.headerId" v-if="item.type === 'header'" :category-name="item.name" />
<div v-else-if="item.type === 'emoji-row'" :key="item.rowId" class="flex gap-1 p-2">
<Emoji v-for="emoji in item.emojis" :key="getEmojiKey(emoji)" :emoji="emoji"
@select="(e) => selectedEmoji = e" @pick="e => {
emit('pick', e); open = false;
}" />
</div>
</VList>
<EmojiDisplay :emoji="selectedEmoji" :style="{
width: `calc(var(--spacing) * ((12 * ${EMOJI_PER_ROW}) + (${EMOJI_PER_ROW} - 1)) + var(--spacing) * 4)`,
}" />
</div>
<Sidebar :categories="categories" @select="scrollToCategory" />
</div>
</PopoverContent>
</Popover>
</template>
<script lang="ts" setup>
import type { CustomEmoji } from "@versia/client/schemas";
import { VList } from "virtua/vue";
import type { z } from "zod";
import { Input } from "~/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import CategoryHeader from "./category-header.vue";
import EmojiDisplay from "./display.vue";
import { type EmojiGroupId, emojiGroups, type UnicodeEmoji } from "./emoji.ts";
import Emoji from "./emoji.vue";
import Sidebar from "./sidebar.vue";
import { EMOJI_PER_ROW, getVirtualizedItems } from "./virtual.ts";
const emit = defineEmits<{
pick: [emoji: z.infer<typeof CustomEmoji> | UnicodeEmoji];
}>();
const open = ref(false);
const selectedEmoji = ref<z.infer<typeof CustomEmoji> | UnicodeEmoji | null>(
null,
);
const emojiContainer = useTemplateRef<HTMLDivElement>("emojiContainer");
const filter = ref("");
const customEmojis = computed(() => identity.value?.emojis ?? []);
const customEmojiCategories = computed(() => {
const categories: Record<string, z.infer<typeof CustomEmoji>[]> = {};
for (const emoji of customEmojis.value) {
const categoryName = emoji.category || "Uncategorized";
if (!categories[categoryName]) {
categories[categoryName] = [];
}
categories[categoryName]?.push(emoji);
}
return categories;
});
const categories = computed(() => {
const customCategories = Object.entries(customEmojiCategories.value).map(
([name, emojis]) => ({
name,
src: (emojis[0]?.url as string) || "",
}),
);
const groupCategories = emojiGroups.map((group) => ({
name: group.id,
groupId: group.id,
}));
return [...customCategories, ...groupCategories];
});
const virtualizedItems = computed(() =>
getVirtualizedItems(customEmojiCategories.value, filter.value),
);
const getEmojiKey = (emoji: z.infer<typeof CustomEmoji> | UnicodeEmoji) => {
if ("url" in emoji) {
return `custom-${emoji.shortcode}`;
}
return `unicode-${emoji.shortcode}`;
};
const scrollToCategory = (category: {
name: string;
groupId?: EmojiGroupId;
src?: string;
}) => {
const categoryId = category.groupId || `custom-${category.name}`;
const headerIndex = virtualizedItems.value.findIndex(
(item) => item.type === "header" && item.categoryId === categoryId,
);
const child = emojiContainer.value?.children[1];
if (headerIndex !== -1 && child) {
// Estimate scroll position based on item heights
// Headers are approximately 38px, emoji rows are approximately 64px
let scrollTop = 0;
for (let i = 0; i < headerIndex; i++) {
const item = virtualizedItems.value[i];
if (item?.type === "header") {
scrollTop += 38;
} else if (item?.type === "emoji-row") {
scrollTop += 64;
}
}
child.scrollTo({
top: scrollTop,
behavior: "smooth",
});
}
};
</script>