RecursiveFolder.svelte 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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. name = name.trim();
  112. if (name === '') {
  113. toast.error("Folder name can't be empty");
  114. return;
  115. }
  116. if (name === folders[folderId].name) {
  117. edit = false;
  118. return;
  119. }
  120. const res = await updateFolderNameById(localStorage.token, folderId, name).catch((error) => {
  121. toast.error(error);
  122. return null;
  123. });
  124. if (res) {
  125. folders[folderId].name = name;
  126. }
  127. };
  128. </script>
  129. {#if dragged && x && y}
  130. <DragGhost {x} {y}>
  131. <div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
  132. <div class="flex items-center gap-1">
  133. <FolderOpen className="size-3.5" strokeWidth="2" />
  134. <div class=" text-xs text-white line-clamp-1">
  135. {folders[folderId].name}
  136. </div>
  137. </div>
  138. </div>
  139. </DragGhost>
  140. {/if}
  141. <div bind:this={folderElement} class="relative {className}" draggable="true">
  142. {#if draggedOver}
  143. <div
  144. 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"
  145. ></div>
  146. {/if}
  147. <Collapsible
  148. bind:open
  149. className="w-full"
  150. buttonClassName="w-full"
  151. on:change={(e) => {
  152. dispatch('open', e.detail);
  153. }}
  154. >
  155. <!-- svelte-ignore a11y-no-static-element-interactions -->
  156. <div class="w-full group">
  157. <button
  158. 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"
  159. on:dblclick={() => {
  160. name = folders[folderId].name;
  161. edit = true;
  162. // focus on the input
  163. setTimeout(() => {
  164. const input = document.getElementById(`folder-${folderId}-input`);
  165. input.focus();
  166. }, 0);
  167. }}
  168. >
  169. <div class="text-gray-300 dark:text-gray-600">
  170. {#if open}
  171. <ChevronDown className=" size-3" strokeWidth="2.5" />
  172. {:else}
  173. <ChevronRight className=" size-3" strokeWidth="2.5" />
  174. {/if}
  175. </div>
  176. <div class="translate-y-[0.5px] flex-1 justify-start text-start">
  177. {#if edit}
  178. <input
  179. id="folder-{folderId}-input"
  180. type="text"
  181. bind:value={name}
  182. on:blur={() => {
  183. edit = false;
  184. nameUpdateHandler();
  185. }}
  186. on:click={(e) => {
  187. // Prevent accidental collapse toggling when clicking inside input
  188. e.stopPropagation();
  189. }}
  190. on:mousedown={(e) => {
  191. // Prevent accidental collapse toggling when clicking inside input
  192. e.stopPropagation();
  193. }}
  194. class="w-full h-full bg-transparent text-gray-500 dark:text-gray-500 outline-none"
  195. />
  196. {:else}
  197. {folders[folderId].name}
  198. {/if}
  199. </div>
  200. <div class=" hidden group-hover:flex dark:text-gray-300">
  201. <button
  202. on:click={(e) => {
  203. e.stopPropagation();
  204. console.log('clicked');
  205. }}
  206. >
  207. <EllipsisHorizontal className="size-4" strokeWidth="2.5" />
  208. </button>
  209. </div>
  210. </button>
  211. </div>
  212. <div slot="content" class="w-full">
  213. {#if folders[folderId].childrenIds || folders[folderId].items?.chat_ids}
  214. <div
  215. class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s dark:border-gray-850"
  216. >
  217. {#if folders[folderId]?.childrenIds}
  218. {#each folders[folderId]?.childrenIds as childId (`${folderId}-${childId}`)}
  219. <svelte:self {folders} folderId={childId} parentDragged={dragged} />
  220. {/each}
  221. {/if}
  222. {#if folders[folderId].items?.chat_ids}
  223. {#each folder.items.chat_ids as chatId (chatId)}
  224. {chatId}
  225. {/each}
  226. {/if}
  227. </div>
  228. {/if}
  229. </div>
  230. </Collapsible>
  231. </div>