| 1 | import { |
| 2 | COMPOSER_MENTION_DRAG_TYPE, |
| 3 | composerMentionFromTreePath, |
| 4 | } from "~/components/chat/composerMentionDrag"; |
| 5 | |
| 6 | interface FileTreeDragTransfer { |
| 7 | setData(format: string, data: string): void; |
| 8 | } |
| 9 | |
| 10 | export interface FileTreeDragStartEvent { |
| 11 | readonly dataTransfer: FileTreeDragTransfer | null; |
| 12 | composedPath(): ReadonlyArray<unknown>; |
| 13 | } |
| 14 | |
| 15 | export interface FileTreeDragMentionHost { |
| 16 | /** Drop the tree's gesture-applied selection of the dragged row. */ |
| 17 | deselect(treePath: string): void; |
| 18 | } |
| 19 | |
| 20 | export interface FileTreeDragMentionController { |
| 21 | /** |
| 22 | * True from the moment a row drag starts until it ends. The tree selects |
| 23 | * the dragged row as part of the gesture; selection changes made while |
| 24 | * this is set are gesture side effects, not requests to open a file. |
| 25 | */ |
| 26 | isDragInProgress(): boolean; |
| 27 | /** Mirror of the tree's current selection, needed for multi-row drags. */ |
| 28 | handleSelectionChange(selectedPaths: ReadonlyArray<string>): void; |
| 29 | handleDragStart(event: FileTreeDragStartEvent): void; |
| 30 | handleDragEnd(): void; |
| 31 | } |
| 32 | |
| 33 | const itemPathOf = (node: unknown): string | null => { |
| 34 | if (typeof node !== "object" || node === null) { |
| 35 | return null; |
| 36 | } |
| 37 | const element = node as { getAttribute?: (name: string) => string | null }; |
| 38 | return typeof element.getAttribute === "function" ? element.getAttribute("data-item-path") : null; |
| 39 | }; |
| 40 | |
| 41 | /** |
| 42 | * Tags file-tree drags with the composer mention payload and keeps the drag |
| 43 | * from acting like a click: while the drag runs, selection changes are |
| 44 | * suppressed, and when it ends the dragged rows are deselected so nothing is |
| 45 | * left highlighted and a later click on them still fires a selection change. |
| 46 | */ |
| 47 | export function createFileTreeDragMentionController( |
| 48 | host: FileTreeDragMentionHost, |
| 49 | ): FileTreeDragMentionController { |
| 50 | let selection: ReadonlyArray<string> = []; |
| 51 | let draggedPaths: ReadonlyArray<string> = []; |
| 52 | return { |
| 53 | isDragInProgress: () => draggedPaths.length > 0, |
| 54 | handleSelectionChange(selectedPaths) { |
| 55 | selection = selectedPaths; |
| 56 | }, |
| 57 | handleDragStart(event) { |
| 58 | if (event.dataTransfer === null) { |
| 59 | return; |
| 60 | } |
| 61 | // Only drags that originate on a tree row are mentions; a text/plain |
| 62 | // fallback would also tag drags of selected text from the panel chrome. |
| 63 | let itemPath: string | null = null; |
| 64 | for (const node of event.composedPath()) { |
| 65 | itemPath = itemPathOf(node); |
| 66 | if (itemPath !== null) { |
| 67 | break; |
| 68 | } |
| 69 | } |
| 70 | if (itemPath === null) { |
| 71 | return; |
| 72 | } |
| 73 | // Same rule the tree applies to the drag itself: dragging a row that is |
| 74 | // part of the current selection drags the whole selection. |
| 75 | const dragged = selection.includes(itemPath) ? selection : [itemPath]; |
| 76 | const mentions = dragged |
| 77 | .map((path) => composerMentionFromTreePath(path)) |
| 78 | .filter((mention): mention is string => mention !== null); |
| 79 | if (mentions.length === 0) { |
| 80 | return; |
| 81 | } |
| 82 | draggedPaths = dragged; |
| 83 | event.dataTransfer.setData(COMPOSER_MENTION_DRAG_TYPE, mentions.join(" ")); |
| 84 | }, |
| 85 | handleDragEnd() { |
| 86 | if (draggedPaths.length === 0) { |
| 87 | return; |
| 88 | } |
| 89 | for (const path of draggedPaths) { |
| 90 | host.deselect(path); |
| 91 | } |
| 92 | draggedPaths = []; |
| 93 | }, |
| 94 | }; |
| 95 | } |