| 1 | import { 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 | */ |
| 8 | export const COMPOSER_MENTION_DRAG_TYPE = "application/x-t3code-composer-mention"; |
| 9 | |
| 10 | export 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 | |
| 18 | export function dataTransferHasComposerMention(types: ReadonlyArray<string>): boolean { |
| 19 | return types.includes(COMPOSER_MENTION_DRAG_TYPE); |
| 20 | } |
| 21 | |
| 22 | export interface ComposerMentionDragTransfer { |
| 23 | readonly types: ReadonlyArray<string>; |
| 24 | getData(format: string): string; |
| 25 | dropEffect: string; |
| 26 | } |
| 27 | |
| 28 | export 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 | */ |
| 42 | export interface ComposerMentionDropHost { |
| 43 | insertMentionAtEnd(text: string): boolean; |
| 44 | setDragActive(active: boolean): void; |
| 45 | onInsertRejected(): void; |
| 46 | } |
| 47 | |
| 48 | export interface ComposerMentionDragHandlers { |
| 49 | onDragEnter(event: ComposerMentionDragEvent): void; |
| 50 | onDragOver(event: ComposerMentionDragEvent): void; |
| 51 | onDrop(event: ComposerMentionDragEvent): void; |
| 52 | } |
| 53 | |
| 54 | export 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 | } |