1import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";
2
3/**
4 * Drag payload type carrying a serialized composer mention. Set on drags that
5 * start in the workspace file tree so the composer can tell them apart from
6 * OS file drags and plain text selections.
7 */
8export const COMPOSER_MENTION_DRAG_TYPE = "application/x-t3code-composer-mention";
9
10export function composerMentionFromTreePath(treePath: string): string | null {
11 const relativePath = treePath.replace(/\/+$/, "");
12 if (relativePath.length === 0) {
13 return null;
14 }
15 return serializeComposerFileLink(relativePath);
16}
17
18export function dataTransferHasComposerMention(types: ReadonlyArray<string>): boolean {
19 return types.includes(COMPOSER_MENTION_DRAG_TYPE);
20}
21
22export interface ComposerMentionDragTransfer {
23 readonly types: ReadonlyArray<string>;
24 getData(format: string): string;
25 dropEffect: string;
26}
27
28export interface ComposerMentionDragEvent {
29 readonly dataTransfer: ComposerMentionDragTransfer;
30 readonly nativeEvent: { stopPropagation(): void };
31 preventDefault(): void;
32 stopPropagation(): void;
33}
34
35/**
36 * What a mention drop is allowed to do to the composer. Deliberately narrow:
37 * there is no way to focus the editor from here. Focusing it synchronously
38 * during the drop makes the not-yet-reconciled editor sync its stale empty
39 * state back over the inserted mention; the insert path already focuses on
40 * the next frame, after the editor has caught up.
41 */
42export interface ComposerMentionDropHost {
43 insertMentionAtEnd(text: string): boolean;
44 setDragActive(active: boolean): void;
45 onInsertRejected(): void;
46}
47
48export interface ComposerMentionDragHandlers {
49 onDragEnter(event: ComposerMentionDragEvent): void;
50 onDragOver(event: ComposerMentionDragEvent): void;
51 onDrop(event: ComposerMentionDragEvent): void;
52}
53
54export function makeComposerMentionDragHandlers(
55 host: ComposerMentionDropHost,
56): ComposerMentionDragHandlers {
57 // Claim the event for the composer: React's stopPropagation only halts the
58 // synthetic dispatch, so the native event must be stopped too or the
59 // editor's own DOM listeners still process the drag.
60 const claim = (event: ComposerMentionDragEvent): boolean => {
61 if (!dataTransferHasComposerMention(event.dataTransfer.types)) {
62 return false;
63 }
64 event.preventDefault();
65 event.stopPropagation();
66 event.nativeEvent.stopPropagation();
67 return true;
68 };
69 return {
70 onDragEnter(event) {
71 if (claim(event)) {
72 host.setDragActive(true);
73 }
74 },
75 onDragOver(event) {
76 if (!claim(event)) {
77 return;
78 }
79 // The tree constrains its drags to effectAllowed "move"; naming any
80 // other effect makes the browser cancel the drop without firing it.
81 event.dataTransfer.dropEffect = "move";
82 host.setDragActive(true);
83 },
84 onDrop(event) {
85 if (!claim(event)) {
86 return;
87 }
88 host.setDragActive(false);
89 const mention = event.dataTransfer.getData(COMPOSER_MENTION_DRAG_TYPE);
90 if (mention.length === 0) {
91 return;
92 }
93 if (!host.insertMentionAtEnd(`${mention} `)) {
94 host.onInsertRejected();
95 }
96 },
97 };
98}