RecursiveFolder.svelte 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. <script>
  2. import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
  3. const i18n = getContext('i18n');
  4. const dispatch = createEventDispatcher();
  5. import ChevronDown from '../../icons/ChevronDown.svelte';
  6. import ChevronRight from '../../icons/ChevronRight.svelte';
  7. import Collapsible from '../../common/Collapsible.svelte';
  8. import DragGhost from '$lib/components/common/DragGhost.svelte';
  9. import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
  10. import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
  11. import { updateFolderNameById } from '$lib/apis/folders';
  12. import { toast } from 'svelte-sonner';
  13. export let open = true;
  14. export let folders;
  15. export let folderId;
  16. export let className = '';
  17. export let parentDragged = false;
  18. let folderElement;
  19. let edit = false;
  20. let draggedOver = false;
  21. let dragged = false;
  22. let name = '';
  23. const onDragOver = (e) => {
  24. e.preventDefault();
  25. e.stopPropagation();
  26. if (dragged || parentDragged) {
  27. return;
  28. }
  29. draggedOver = true;
  30. };
  31. const onDrop = (e) => {
  32. e.preventDefault();
  33. e.stopPropagation();
  34. if (dragged || parentDragged) {
  35. return;
  36. }
  37. if (folderElement.contains(e.target)) {
  38. console.log('Dropped on the Button');
  39. try {
  40. // get data from the drag event
  41. const dataTransfer = e.dataTransfer.getData('text/plain');
  42. const data = JSON.parse(dataTransfer);
  43. console.log(data);
  44. dispatch('drop', data);
  45. } catch (error) {
  46. console.error(error);
  47. }
  48. draggedOver = false;
  49. }
  50. };
  51. const onDragLeave = (e) => {
  52. e.preventDefault();
  53. if (dragged || parentDragged) {
  54. return;
  55. }
  56. draggedOver = false;
  57. };
  58. const dragImage = new Image();
  59. dragImage.src =
  60. 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
  61. let x;
  62. let y;
  63. const onDragStart = (event) => {
  64. event.stopPropagation();
  65. event.dataTransfer.setDragImage(dragImage, 0, 0);
  66. // Set the data to be transferred
  67. event.dataTransfer.setData(
  68. 'text/plain',
  69. JSON.stringify({
  70. type: 'folder',
  71. id: folderId
  72. })
  73. );
  74. dragged = true;
  75. folderElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
  76. };
  77. const onDrag = (event) => {
  78. event.stopPropagation();
  79. x = event.clientX;
  80. y = event.clientY;
  81. };
  82. const onDragEnd = (event) => {
  83. event.stopPropagation();
  84. folderElement.style.opacity = '1'; // Reset visual cue after drag
  85. dragged = false;
  86. };
  87. onMount(() => {
  88. if (folderElement) {
  89. folderElement.addEventListener('dragover', onDragOver);
  90. folderElement.addEventListener('drop', onDrop);
  91. folderElement.addEventListener('dragleave', onDragLeave);
  92. // Event listener for when dragging starts
  93. folderElement.addEventListener('dragstart', onDragStart);
  94. // Event listener for when dragging occurs (optional)
  95. folderElement.addEventListener('drag', onDrag);
  96. // Event listener for when dragging ends
  97. folderElement.addEventListener('dragend', onDragEnd);
  98. }
  99. });
  100. onDestroy(() => {
  101. if (folderElement) {
  102. folderElement.addEventListener('dragover', onDragOver);
  103. folderElement.removeEventListener('drop', onDrop);
  104. folderElement.removeEventListener('dragleave', onDragLeave);
  105. folderElement.removeEventListener('dragstart', onDragStart);
  106. folderElement.removeEventListener('drag', onDrag);
  107. folderElement.removeEventListener('dragend', onDragEnd);
  108. }
  109. });
  110. const nameUpdateHandler = async () => {
  111. if (name === '') {
  112. toast.error("Folder name can't be empty");
  113. return;
  114. }
  115. if (name === folders[folderId].name) {
  116. edit = false;
  117. return;
  118. }
  119. const currentName = folders[folderId].name;
  120. name = name.trim();
  121. folders[folderId].name = name;
  122. const res = await updateFolderNameById(localStorage.token, folderId, name).catch((error) => {
  123. toast.error(error);
  124. folders[folderId].name = currentName;
  125. return null;
  126. });
  127. if (res) {
  128. folders[folderId].name = name;
  129. }
  130. };
  131. </script>
  132. {#if dragged && x && y}
  133. <DragGhost {x} {y}>
  134. <div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
  135. <div class="flex items-center gap-1">
  136. <FolderOpen className="size-3.5" strokeWidth="2" />
  137. <div class=" text-xs text-white line-clamp-1">
  138. {folders[folderId].name}
  139. </div>
  140. </div>
  141. </div>
  142. </DragGhost>
  143. {/if}
  144. <div bind:this={folderElement} class="relative {className}" draggable="true">
  145. {#if draggedOver}
  146. <div
  147. class="absolute top-0 left-0 w-full h-full rounded-sm bg-[hsla(258,88%,66%,0.1)] bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
  148. ></div>
  149. {/if}
  150. <Collapsible
  151. bind:open
  152. className="w-full"
  153. buttonClassName="w-full"
  154. on:change={(e) => {
  155. dispatch('open', e.detail);
  156. }}
  157. >
  158. <!-- svelte-ignore a11y-no-static-element-interactions -->
  159. <div class="w-full group">
  160. <button
  161. class="w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
  162. on:dblclick={() => {
  163. name = folders[folderId].name;
  164. edit = true;
  165. // focus on the input
  166. setTimeout(() => {
  167. const input = document.getElementById(`folder-${folderId}-input`);
  168. input.focus();
  169. }, 0);
  170. }}
  171. >
  172. <div class="text-gray-300 dark:text-gray-600">
  173. {#if open}
  174. <ChevronDown className=" size-3" strokeWidth="2.5" />
  175. {:else}
  176. <ChevronRight className=" size-3" strokeWidth="2.5" />
  177. {/if}
  178. </div>
  179. <div class="translate-y-[0.5px] flex-1 justify-start text-start">
  180. {#if edit}
  181. <input
  182. id="folder-{folderId}-input"
  183. type="text"
  184. bind:value={name}
  185. on:blur={() => {
  186. edit = false;
  187. nameUpdateHandler();
  188. }}
  189. on:click={(e) => {
  190. // Prevent accidental collapse toggling when clicking inside input
  191. e.stopPropagation();
  192. }}
  193. on:mousedown={(e) => {
  194. // Prevent accidental collapse toggling when clicking inside input
  195. e.stopPropagation();
  196. }}
  197. class="w-full h-full bg-transparent text-gray-500 dark:text-gray-500 outline-none"
  198. />
  199. {:else}
  200. {folders[folderId].name}
  201. {/if}
  202. </div>
  203. <div class=" hidden group-hover:flex dark:text-gray-300">
  204. <button
  205. on:click={(e) => {
  206. e.stopPropagation();
  207. console.log('clicked');
  208. }}
  209. >
  210. <EllipsisHorizontal className="size-4" strokeWidth="2.5" />
  211. </button>
  212. </div>
  213. </button>
  214. </div>
  215. <div slot="content" class="w-full">
  216. {#if folders[folderId].childrenIds || folders[folderId].items?.chat_ids}
  217. <div
  218. class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s dark:border-gray-850"
  219. >
  220. {#if folders[folderId]?.childrenIds}
  221. {#each folders[folderId]?.childrenIds as childId (`${folderId}-${childId}`)}
  222. <svelte:self {folders} folderId={childId} parentDragged={dragged} />
  223. {/each}
  224. {/if}
  225. {#if folders[folderId].items?.chat_ids}
  226. {#each folder.items.chat_ids as chatId (chatId)}
  227. {chatId}
  228. {/each}
  229. {/if}
  230. </div>
  231. {/if}
  232. </div>
  233. </Collapsible>
  234. </div>