From 8b9a103fd3681a06738db2cb03cc8203972638d2 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 7 Aug 2024 12:32:41 -0700 Subject: [PATCH] Extract useDragDropDataTransfer --- .../chat/components/composer/Composer.tsx | 10 +- .../composer/useComposerDragDrop.tsx | 109 ++-------------- .../components/useDragDropDataTransfer.tsx | 118 ++++++++++++++++++ 3 files changed, 130 insertions(+), 107 deletions(-) create mode 100644 src/common/components/useDragDropDataTransfer.tsx diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index b2530f199..433bd2c05 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -73,8 +73,8 @@ import { ComposerTextAreaActions } from './textarea/ComposerTextAreaActions'; import { StatusBar } from '../StatusBar'; import { TokenBadgeMemo } from './tokens/TokenBadge'; import { TokenProgressbarMemo } from './tokens/TokenProgressbar'; +import { useComposerDragDrop } from './useComposerDragDrop'; import { useComposerStartupText } from './store-composer'; -import { useDragDrop } from './useComposerDragDrop'; const zIndexComposerOverlayMic = 10; @@ -159,7 +159,7 @@ export function Composer(props: { const llmAttachmentDraftsCollection = useLLMAttachmentDrafts(attachmentDrafts, props.chatLLM); // drag/drop - const { dragContainerSx, dragDropComponent, handleDragEnter, handleDragStart } = useDragDrop(!!props.isMobile, attachAppendDataTransfer); + const { dragContainerSx, dropComponent, handleContainerDragEnter, handleContainerDragStart } = useComposerDragDrop(!props.isMobile, attachAppendDataTransfer); // ai functions const agiAttachmentPrompts = useAgiAttachmentPrompts(useChatAutoSuggestAttachmentPrompts(), attachmentDrafts); @@ -572,8 +572,8 @@ export function Composer(props: { @@ -877,7 +877,7 @@ export function Composer(props: { {/* overlay: Drag & Drop*/} - {dragDropComponent} + {dropComponent} diff --git a/src/apps/chat/components/composer/useComposerDragDrop.tsx b/src/apps/chat/components/composer/useComposerDragDrop.tsx index a919894f3..37abe5bfa 100644 --- a/src/apps/chat/components/composer/useComposerDragDrop.tsx +++ b/src/apps/chat/components/composer/useComposerDragDrop.tsx @@ -1,84 +1,15 @@ import * as React from 'react'; -import type { SxProps } from '@mui/joy/styles/types'; -import { Card, Typography } from '@mui/joy'; -import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded'; +import { useDragDropDataTransfer } from '~/common/components/useDragDropDataTransfer'; -const zIndexComposerOverlayDrop = 20; - - -const containerSx: SxProps = { - position: 'relative', /* for Drop overlay */ -} as const; - -const inactiveCardSx: SxProps = { - display: 'none', - position: 'absolute', - inset: 0, - pointerEvents: 'none', - zIndex: zIndexComposerOverlayDrop, -} as const; - -const draggingCardSx: SxProps = { - ...inactiveCardSx, - pointerEvents: undefined, - border: '1px dashed', - borderRadius: 'sm', - boxShadow: 'inset 1px 0px 3px -2px var(--joy-palette-success-softColor)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - gap: 2, -} as const; - - -export function useDragDrop( - disabled: boolean, +export function useComposerDragDrop( + enabled: boolean, onDataTransfer: (dataTransfer: DataTransfer, type: 'paste' | 'drop', isDropOnTextarea: boolean) => Promise, ) { - // state - const [isDragging, setIsDragging] = React.useState(false); - - - // handlers - - const eatDragEvent = React.useCallback((event: React.DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - }, []); - - - const handleExtDragEnter = React.useCallback((event: React.DragEvent) => { - const isFromSelf = event.dataTransfer.types.includes('x-app/agi'); - if (!isFromSelf) { - eatDragEvent(event); - setIsDragging(true); - } - }, [eatDragEvent]); - - const handleExtDragStart = React.useCallback((event: React.DragEvent) => { - event.dataTransfer.setData('x-app/agi', 'do-not-intercept'); - }, []); - - - const handleDragLeave = React.useCallback((event: React.DragEvent) => { - eatDragEvent(event); - setIsDragging(false); - }, [eatDragEvent]); - - const handleDragOver = React.useCallback((event: React.DragEvent) => { - eatDragEvent(event); - // this makes sure we don't "transfer" (or move) the item, but we tell the sender we'll copy it - event.dataTransfer.dropEffect = 'copy'; - }, [eatDragEvent]); - - const handleDrop = React.useCallback(async (event: React.DragEvent) => { - eatDragEvent(event); - setIsDragging(false); - - const { dataTransfer } = event; + // drop implementation for the composer + const handleComposerDrop = React.useCallback(async (dataTransfer: DataTransfer) => { // VSCode: detect failure of dropping from VSCode, details below: // https://github.com/microsoft/vscode/issues/98629#issuecomment-634475572 @@ -106,34 +37,8 @@ export function useDragDrop( // textarea drop void onDataTransfer(dataTransfer, 'drop', true); // fire/forget - }, [eatDragEvent, onDataTransfer]); + }, [onDataTransfer]); - const dropComponent = React.useMemo(() => { - if (disabled) return null; - - return ( - - {isDragging && } - {isDragging && - I will hold on to this for you. - } - - ); - }, [handleDragLeave, handleDragOver, handleDrop, isDragging, disabled]); - - return { - dragContainerSx: containerSx, - dragDropComponent: dropComponent, - handleDragEnter: handleExtDragEnter, - handleDragStart: handleExtDragStart, - }; + return useDragDropDataTransfer(enabled, 'I will hold on to this for you.', handleComposerDrop); } diff --git a/src/common/components/useDragDropDataTransfer.tsx b/src/common/components/useDragDropDataTransfer.tsx new file mode 100644 index 000000000..574751d80 --- /dev/null +++ b/src/common/components/useDragDropDataTransfer.tsx @@ -0,0 +1,118 @@ +import * as React from 'react'; + +import type { SxProps } from '@mui/joy/styles/types'; +import { Card, Typography } from '@mui/joy'; +import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded'; + + +// constants +const zIndexComposerOverlayDrop = 20; +const EXCLUDE_SELF_TYPE = 'x-app/agi'; + + +// styles + +const dragContainerSx: SxProps = { + position: 'relative', /* for Drop overlay */ +} as const; + +const dropCardInactiveSx: SxProps = { + display: 'none', + position: 'absolute', + inset: 0, + pointerEvents: 'none', + zIndex: zIndexComposerOverlayDrop, +} as const; + +const dropCardDraggingCardSx: SxProps = { + ...dropCardInactiveSx, + pointerEvents: undefined, + border: '1px dashed', + borderRadius: 'sm', + boxShadow: 'inset 1px 0px 3px -2px var(--joy-palette-success-softColor)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 2, +} as const; + + +// Drag/Drop that can be used in any component and invokes a DataTransfer callback on success + +export function useDragDropDataTransfer(enabled: boolean, dropText: string, onDropCallback: (dataTransfer: DataTransfer) => Promise) { + + // state + const [isDragging, setIsDragging] = React.useState(false); + + + const _eatDragEvent = React.useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + }, []); + + + // Container events + + const handleContainerDragEnter = React.useCallback((event: React.DragEvent) => { + const isFromSelf = event.dataTransfer.types.includes(EXCLUDE_SELF_TYPE); + if (!isFromSelf) { + _eatDragEvent(event); + setIsDragging(true); + } + }, [_eatDragEvent]); + + const handleContainerDragStart = React.useCallback((event: React.DragEvent) => { + event.dataTransfer.setData(EXCLUDE_SELF_TYPE, 'do-not-intercept'); + }, []); + + + // Drop Target events + + const _handleDragOver = React.useCallback((event: React.DragEvent) => { + _eatDragEvent(event); + // this makes sure we don't "transfer" (or move) the item, but we tell the sender we'll copy it + event.dataTransfer.dropEffect = 'copy'; + }, [_eatDragEvent]); + + const _handleDragLeave = React.useCallback((event: React.DragEvent) => { + _eatDragEvent(event); + setIsDragging(false); + }, [_eatDragEvent]); + + const _handleDrop = React.useCallback(async (event: React.DragEvent) => { + _eatDragEvent(event); + setIsDragging(false); + await onDropCallback(event.dataTransfer); + }, [_eatDragEvent, onDropCallback]); + + + const dropComponent = React.useMemo(() => { + if (!enabled) return null; + + return ( + + {isDragging && } + {isDragging && + {dropText} + } + + ); + }, [enabled, isDragging, _handleDragLeave, _handleDragOver, _handleDrop, dropText]); + + + return { + dragContainerSx, + dropComponent, + handleContainerDragEnter, + handleContainerDragStart, + isDragging, + }; +}