ReactionPicker.svelte 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <script lang="ts">
  2. import { DropdownMenu } from 'bits-ui';
  3. import { flyAndScale } from '$lib/utils/transitions';
  4. import emojiGroups from '$lib/emoji-groups.json';
  5. import emojiShortCodes from '$lib/emoji-shortcodes.json';
  6. import Tooltip from '$lib/components/common/Tooltip.svelte';
  7. import VirtualList from '@sveltejs/svelte-virtual-list';
  8. export let onClose = () => {};
  9. export let onSubmit = (name) => {};
  10. export let side = 'top';
  11. export let align = 'start';
  12. export let user = null;
  13. let show = false;
  14. let emojis = emojiShortCodes;
  15. let search = '';
  16. let flattenedEmojis = [];
  17. let emojiRows = [];
  18. // Reactive statement to filter the emojis based on search query
  19. $: {
  20. if (search) {
  21. emojis = Object.keys(emojiShortCodes).reduce((acc, key) => {
  22. if (key.includes(search)) {
  23. acc[key] = emojiShortCodes[key];
  24. } else {
  25. if (Array.isArray(emojiShortCodes[key])) {
  26. const filtered = emojiShortCodes[key].filter((emoji) => emoji.includes(search));
  27. if (filtered.length) {
  28. acc[key] = filtered;
  29. }
  30. } else {
  31. if (emojiShortCodes[key].includes(search)) {
  32. acc[key] = emojiShortCodes[key];
  33. }
  34. }
  35. }
  36. return acc;
  37. }, {});
  38. } else {
  39. emojis = emojiShortCodes;
  40. }
  41. }
  42. // Flatten emoji groups and group them into rows of 8 for virtual scrolling
  43. $: {
  44. flattenedEmojis = [];
  45. Object.keys(emojiGroups).forEach((group) => {
  46. const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji]);
  47. if (groupEmojis.length > 0) {
  48. flattenedEmojis.push({ type: 'group', label: group });
  49. flattenedEmojis.push(
  50. ...groupEmojis.map((emoji) => ({
  51. type: 'emoji',
  52. name: emoji,
  53. shortCodes:
  54. typeof emojiShortCodes[emoji] === 'string'
  55. ? [emojiShortCodes[emoji]]
  56. : emojiShortCodes[emoji]
  57. }))
  58. );
  59. }
  60. });
  61. // Group emojis into rows of 6
  62. emojiRows = [];
  63. let currentRow = [];
  64. flattenedEmojis.forEach((item) => {
  65. if (item.type === 'emoji') {
  66. currentRow.push(item);
  67. if (currentRow.length === 7) {
  68. emojiRows.push(currentRow);
  69. currentRow = [];
  70. }
  71. } else if (item.type === 'group') {
  72. if (currentRow.length > 0) {
  73. emojiRows.push(currentRow); // Push the remaining row
  74. currentRow = [];
  75. }
  76. emojiRows.push([item]); // Add the group label as a separate row
  77. }
  78. });
  79. if (currentRow.length > 0) {
  80. emojiRows.push(currentRow); // Push the final row
  81. }
  82. }
  83. const ROW_HEIGHT = 48; // Approximate height for a row with multiple emojis
  84. // Handle emoji selection
  85. function selectEmoji(emoji) {
  86. const selectedCode = emoji.shortCodes[0];
  87. onSubmit(selectedCode);
  88. show = false;
  89. }
  90. </script>
  91. <DropdownMenu.Root
  92. bind:open={show}
  93. closeFocus={false}
  94. onOpenChange={(state) => {
  95. if (!state) {
  96. search = '';
  97. onClose();
  98. }
  99. }}
  100. typeahead={false}
  101. >
  102. <DropdownMenu.Trigger>
  103. <slot />
  104. </DropdownMenu.Trigger>
  105. <DropdownMenu.Content
  106. class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white"
  107. sideOffset={8}
  108. {side}
  109. {align}
  110. transition={flyAndScale}
  111. >
  112. <div class="mb-1 px-3 pt-2 pb-2">
  113. <input
  114. type="text"
  115. class="w-full text-sm bg-transparent outline-none"
  116. placeholder="Search all emojis"
  117. bind:value={search}
  118. />
  119. </div>
  120. <!-- Virtualized Emoji List -->
  121. <div class="w-full flex justify-start h-96 overflow-y-auto px-3 pb-3 text-sm">
  122. {#if emojiRows.length === 0}
  123. <div class="text-center text-xs text-gray-500 dark:text-gray-400">No results</div>
  124. {:else}
  125. <div class="w-full flex ml-2">
  126. <VirtualList rowHeight={ROW_HEIGHT} items={emojiRows} height={384} let:item>
  127. <div class="w-full">
  128. {#if item.length === 1 && item[0].type === 'group'}
  129. <!-- Render group header -->
  130. <div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400">
  131. {item[0].label}
  132. </div>
  133. {:else}
  134. <!-- Render emojis in a row -->
  135. <div class="flex items-center gap-2 w-full">
  136. {#each item as emojiItem}
  137. <Tooltip
  138. content={emojiItem.shortCodes.map((code) => `:${code}:`).join(', ')}
  139. placement="top"
  140. >
  141. <button
  142. class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition"
  143. on:click={() => selectEmoji(emojiItem)}
  144. >
  145. <img
  146. src="/assets/emojis/{emojiItem.name.toLowerCase()}.svg"
  147. alt={emojiItem.name}
  148. class="size-5"
  149. loading="lazy"
  150. />
  151. </button>
  152. </Tooltip>
  153. {/each}
  154. </div>
  155. {/if}
  156. </div>
  157. </VirtualList>
  158. </div>
  159. {/if}
  160. </div>
  161. </DropdownMenu.Content>
  162. </DropdownMenu.Root>