Files
cinny/src/app/features/lobby/DnD.tsx
T

147 lines
3.9 KiB
TypeScript
Raw Normal View History

import React, { RefObject, useEffect, useRef, useState } from 'react';
import {
dropTargetForElements,
draggable,
monitorForElements,
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import classNames from 'classnames';
import { Box, Icon, Icons, as } from 'folds';
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import * as css from './DnD.css';
export type DropContainerData = {
item: HierarchyItem;
nextRoomId?: string;
};
export type CanDropCallback = (item: HierarchyItem, container: DropContainerData) => boolean;
export const useDraggableItem = (
item: HierarchyItem,
targetRef: RefObject<HTMLElement>,
onDragging: (item?: HierarchyItem) => void,
dragHandleRef?: RefObject<HTMLElement>
): boolean => {
const [dragging, setDragging] = useState(false);
useEffect(() => {
const target = targetRef.current;
const dragHandle = dragHandleRef?.current ?? undefined;
return !target
? undefined
: draggable({
element: target,
dragHandle,
getInitialData: () => item,
onDragStart: () => {
setDragging(true);
onDragging(item);
},
onDrop: () => {
setDragging(false);
onDragging(undefined);
},
});
}, [targetRef, dragHandleRef, item, onDragging]);
return dragging;
};
export const ItemDraggableTarget = as<'div'>(({ className, ...props }, ref) => (
<Box
justifyContent="Center"
alignItems="Center"
className={classNames(css.ItemDraggableTarget, className)}
ref={ref}
{...props}
>
<Icon size="50" src={Icons.VerticalDots} />
</Box>
));
type AfterItemDropTargetProps = {
item: HierarchyItem;
afterSpace?: boolean;
nextRoomId?: string;
canDrop: CanDropCallback;
};
export function AfterItemDropTarget({
item,
afterSpace,
nextRoomId,
canDrop,
}: AfterItemDropTargetProps) {
const targetRef = useRef<HTMLDivElement>(null);
const [dropState, setDropState] = useState<'idle' | 'allow' | 'not-allow'>('idle');
useEffect(() => {
const target = targetRef.current;
if (!target) {
throw Error('drop target ref is not set properly');
}
return dropTargetForElements({
element: target,
getData: () => {
const container: DropContainerData = {
item,
nextRoomId,
};
return container;
},
onDragEnter: ({ source }) => {
if (
canDrop(source.data as HierarchyItem, {
item,
nextRoomId,
})
) {
setDropState('allow');
} else {
setDropState('not-allow');
}
},
onDragLeave: () => setDropState('idle'),
onDrop: () => setDropState('idle'),
});
}, [item, nextRoomId, canDrop]);
return (
<div
className={afterSpace ? css.AfterSpaceItemDropTarget : css.AfterRoomItemDropTarget}
data-hover={dropState !== 'idle'}
data-error={dropState === 'not-allow'}
ref={targetRef}
/>
);
}
export const useDnDMonitor = (
scrollRef: RefObject<HTMLElement>,
onDragging: (item?: HierarchyItem) => void,
onReorder: (item: HierarchyItem, container: DropContainerData) => void
) => {
useEffect(() => {
const scrollElement = scrollRef.current;
if (!scrollElement) {
throw Error('Scroll element ref not configured');
}
return combine(
monitorForElements({
onDrop: ({ source, location }) => {
onDragging(undefined);
const { dropTargets } = location.current;
if (dropTargets.length === 0) return;
onReorder(source.data as HierarchyItem, dropTargets[0].data as DropContainerData);
},
}),
autoScrollForElements({
element: scrollElement,
})
);
}, [scrollRef, onDragging, onReorder]);
};