1import {
2 COMPOSER_MENTION_DRAG_TYPE,
3 composerMentionFromTreePath,
4} from "~/components/chat/composerMentionDrag";
5
6interface FileTreeDragTransfer {
7 setData(format: string, data: string): void;
8}
9
10export interface FileTreeDragStartEvent {
11 readonly dataTransfer: FileTreeDragTransfer | null;
12 composedPath(): ReadonlyArray<unknown>;
13}
14
15export interface FileTreeDragMentionHost {
16 /** The path a dragged row mentions; defaults to the tree path itself. */
17 mentionPath?(treePath: string): string;
18}
19
20export interface FileTreeDragMentionController {
21 isDragInProgress(): boolean;
22 handleSelectionChange(selectedPaths: ReadonlyArray<string>): void;
23 handleDragStart(event: FileTreeDragStartEvent): void;
24 handleDragEnd(deselect: (treePath: string) => void): void;
25}
26
27const itemPathOf = (node: unknown): string | null => {
28 if (typeof node !== "object" || node === null) {
29 return null;
30 }
31 const element = node as { getAttribute?: (name: string) => string | null };
32 return typeof element.getAttribute === "function" ? element.getAttribute("data-item-path") : null;
33};
34
35export function createFileTreeDragMentionController(
36 host: FileTreeDragMentionHost,
37): FileTreeDragMentionController {
38 let selection: ReadonlyArray<string> = [];
39 let draggedPaths: ReadonlyArray<string> = [];
40 return {
41 isDragInProgress: () => draggedPaths.length > 0,
42 handleSelectionChange(selectedPaths) {
43 selection = selectedPaths;
44 },
45 handleDragStart(event) {
46 if (event.dataTransfer === null) {
47 return;
48 }
49 // Require a tree-row origin; text/plain fallback would misclassify selected panel text as a mention.
50 let itemPath: string | null = null;
51 for (const node of event.composedPath()) {
52 itemPath = itemPathOf(node);
53 if (itemPath !== null) {
54 break;
55 }
56 }
57 if (itemPath === null) {
58 return;
59 }
60 const dragged = selection.includes(itemPath) ? selection : [itemPath];
61 const mentions = dragged
62 .map((path) => {
63 const treePath = path.replace(/\/+$/, "");
64 return composerMentionFromTreePath(
65 treePath && host.mentionPath ? host.mentionPath(treePath) : path,
66 );
67 })
68 .filter((mention): mention is string => mention !== null);
69 if (mentions.length === 0) {
70 return;
71 }
72 draggedPaths = dragged;
73 event.dataTransfer.setData(COMPOSER_MENTION_DRAG_TYPE, mentions.join(" "));
74 },
75 handleDragEnd(deselect) {
76 if (draggedPaths.length === 0) {
77 return;
78 }
79 for (const path of draggedPaths) {
80 deselect(path);
81 }
82 draggedPaths = [];
83 },
84 };
85}