mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Extract useDragDropDataTransfer
This commit is contained in:
@@ -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: {
|
||||
|
||||
<Grid
|
||||
container
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnter={handleContainerDragEnter}
|
||||
onDragStart={handleContainerDragStart}
|
||||
spacing={{ xs: 1, md: 2 }}
|
||||
sx={stableGridSx}
|
||||
>
|
||||
@@ -877,7 +877,7 @@ export function Composer(props: {
|
||||
</Grid>
|
||||
|
||||
{/* overlay: Drag & Drop*/}
|
||||
{dragDropComponent}
|
||||
{dropComponent}
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -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<any>,
|
||||
) {
|
||||
|
||||
// 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 (
|
||||
<Card
|
||||
color={isDragging ? 'success' : undefined}
|
||||
variant={isDragging ? 'soft' : undefined}
|
||||
invertedColors={isDragging}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
sx={isDragging ? draggingCardSx : inactiveCardSx}
|
||||
>
|
||||
{isDragging && <AttachFileRoundedIcon sx={{ width: 36, height: 36, pointerEvents: 'none' }} />}
|
||||
{isDragging && <Typography level='title-sm' sx={{ pointerEvents: 'none' }}>
|
||||
I will hold on to this for you.
|
||||
</Typography>}
|
||||
</Card>
|
||||
);
|
||||
}, [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);
|
||||
}
|
||||
|
||||
@@ -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<any>) {
|
||||
|
||||
// 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 (
|
||||
<Card
|
||||
color={isDragging ? 'success' : undefined}
|
||||
variant={isDragging ? 'soft' : undefined}
|
||||
invertedColors={isDragging}
|
||||
onDragLeave={_handleDragLeave}
|
||||
onDragOver={_handleDragOver}
|
||||
onDrop={_handleDrop}
|
||||
sx={isDragging ? dropCardDraggingCardSx : dropCardInactiveSx}
|
||||
>
|
||||
{isDragging && <AttachFileRoundedIcon sx={{ width: 36, height: 36, pointerEvents: 'none' }} />}
|
||||
{isDragging && <Typography level='title-sm' sx={{ pointerEvents: 'none' }}>
|
||||
{dropText}
|
||||
</Typography>}
|
||||
</Card>
|
||||
);
|
||||
}, [enabled, isDragging, _handleDragLeave, _handleDragOver, _handleDrop, dropText]);
|
||||
|
||||
|
||||
return {
|
||||
dragContainerSx,
|
||||
dropComponent,
|
||||
handleContainerDragEnter,
|
||||
handleContainerDragStart,
|
||||
isDragging,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user