forked from github/plane
10ed12e589
* pragmatic drag n drop implementation for labels * minor code quality improvements
162 lines
6.2 KiB
TypeScript
162 lines
6.2 KiB
TypeScript
import { MutableRefObject, useEffect, useRef, useState } from "react";
|
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
|
|
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
|
|
import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
|
import { observer } from "mobx-react";
|
|
import { createRoot } from "react-dom/client";
|
|
// types
|
|
import { IIssueLabel, InstructionType } from "@plane/types";
|
|
// ui
|
|
import { DropIndicator } from "@plane/ui";
|
|
// components
|
|
import { LabelName } from "./label-block/label-name";
|
|
import { TargetData, getCanDrop, getInstructionFromPayload } from "./label-utils";
|
|
|
|
type LabelDragPreviewProps = {
|
|
label: IIssueLabel;
|
|
isGroup: boolean;
|
|
};
|
|
|
|
export const LabelDragPreview = (props: LabelDragPreviewProps) => {
|
|
const { label, isGroup } = props;
|
|
|
|
return (
|
|
<div className="py-3 pl-2 pr-4 border-[1px] border-custom-border-200 bg-custom-background-100">
|
|
<LabelName name={label.name} color={label.color} isGroup={isGroup} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type Props = {
|
|
label: IIssueLabel;
|
|
isGroup: boolean;
|
|
isChild: boolean;
|
|
isLastChild: boolean;
|
|
children: (
|
|
isDragging: boolean,
|
|
isDroppingInLabel: boolean,
|
|
dragHandleRef: MutableRefObject<HTMLButtonElement | null>
|
|
) => JSX.Element;
|
|
onDrop: (
|
|
draggingLabelId: string,
|
|
droppedParentId: string | null,
|
|
droppedLabelId: string | undefined,
|
|
dropAtEndOfList: boolean
|
|
) => void;
|
|
};
|
|
|
|
export const LabelDndHOC = observer((props: Props) => {
|
|
const { label, isGroup, isChild, isLastChild, children, onDrop } = props;
|
|
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined);
|
|
// refs
|
|
const labelRef = useRef<HTMLDivElement | null>(null);
|
|
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
const element = labelRef.current;
|
|
const dragHandleElement = dragHandleRef.current;
|
|
|
|
if (!element) return;
|
|
|
|
return combine(
|
|
draggable({
|
|
element,
|
|
dragHandle: dragHandleElement ?? undefined,
|
|
getInitialData: () => ({ id: label?.id, parentId: label?.parent, isGroup, isChild }),
|
|
onDragStart: () => {
|
|
setIsDragging(true);
|
|
},
|
|
onDrop: () => {
|
|
setIsDragging(false);
|
|
},
|
|
onGenerateDragPreview: ({ nativeSetDragImage }) => {
|
|
setCustomNativeDragPreview({
|
|
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
|
|
render: ({ container }) => {
|
|
const root = createRoot(container);
|
|
root.render(<LabelDragPreview label={label} isGroup={isGroup} />);
|
|
return () => root.unmount();
|
|
},
|
|
nativeSetDragImage,
|
|
});
|
|
},
|
|
}),
|
|
dropTargetForElements({
|
|
element,
|
|
canDrop: ({ source }) => getCanDrop(source, label, isChild),
|
|
getData: ({ input, element }) => {
|
|
const data = { id: label?.id, parentId: label?.parent, isGroup, isChild };
|
|
|
|
const blockedStates: InstructionType[] = [];
|
|
|
|
// if is currently a child then block make-child instruction
|
|
if (isChild) blockedStates.push("make-child");
|
|
// if is currently is not a last child then block reorder-below instruction
|
|
if (!isLastChild) blockedStates.push("reorder-below");
|
|
|
|
return attachInstruction(data, {
|
|
input,
|
|
element,
|
|
currentLevel: isChild ? 1 : 0,
|
|
indentPerLevel: 0,
|
|
mode: isLastChild ? "last-in-group" : "standard",
|
|
block: blockedStates,
|
|
});
|
|
},
|
|
onDrag: ({ self, source, location }) => {
|
|
const instruction = getInstructionFromPayload(self, source, location);
|
|
setInstruction(instruction);
|
|
},
|
|
onDragLeave: () => {
|
|
setInstruction(undefined);
|
|
},
|
|
onDrop: ({ source, location }) => {
|
|
setInstruction(undefined);
|
|
|
|
const dropTargets = location?.current?.dropTargets ?? [];
|
|
|
|
if (isChild || !dropTargets || dropTargets.length <= 0) return;
|
|
|
|
// if the label is dropped on both a child and it's parent at the same time then get only the child's drop target
|
|
const dropTarget =
|
|
dropTargets.length > 1 ? dropTargets.find((target) => target?.data?.isChild) : dropTargets[0];
|
|
|
|
let parentId: string | null = null,
|
|
dropAtEndOfList = false;
|
|
|
|
const dropTargetData = dropTarget?.data as TargetData;
|
|
|
|
if (!dropTarget || !dropTargetData) return;
|
|
|
|
// get possible instructions for the dropTarget
|
|
const instruction = getInstructionFromPayload(dropTarget, source, location);
|
|
|
|
// if instruction is make child the set parentId as current dropTarget Id or else set it as dropTarget's parentId
|
|
parentId = instruction === "make-child" ? dropTargetData.id : dropTargetData.parentId;
|
|
// if instruction is any other than make-child, i.e., reorder-above and reorder-below then set the droppedId as dropTarget's id
|
|
const droppedLabelId = instruction !== "make-child" ? dropTargetData.id : undefined;
|
|
// if instruction is to reorder-below that is enabled only for end of the last items in the list then dropAtEndOfList as true
|
|
if (instruction === "reorder-below") dropAtEndOfList = true;
|
|
|
|
const sourceData = source.data as TargetData;
|
|
if (sourceData.id) onDrop(sourceData.id as string, parentId, droppedLabelId, dropAtEndOfList);
|
|
},
|
|
})
|
|
);
|
|
}, [labelRef?.current, dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
|
|
|
|
const isMakeChild = instruction == "make-child";
|
|
|
|
return (
|
|
<div ref={labelRef}>
|
|
<DropIndicator classNames="my-1" isVisible={instruction === "reorder-above"} />
|
|
{children(isDragging, isMakeChild, dragHandleRef)}
|
|
{isLastChild && <DropIndicator classNames="my-1" isVisible={instruction === "reorder-below"} />}
|
|
</div>
|
|
);
|
|
});
|