2021-05-27 23:13:21 -04:00
|
|
|
import React, { useState, useContext } from "react";
|
|
|
|
import {
|
2021-06-09 09:25:28 -04:00
|
|
|
MouseSensor,
|
|
|
|
TouchSensor,
|
2021-06-08 20:33:47 -04:00
|
|
|
KeyboardSensor,
|
2021-05-27 23:13:21 -04:00
|
|
|
useSensor,
|
|
|
|
useSensors,
|
|
|
|
closestCenter,
|
|
|
|
} from "@dnd-kit/core";
|
|
|
|
|
2021-06-10 04:04:32 -04:00
|
|
|
import DragContext from "./DragContext";
|
|
|
|
|
2021-05-27 23:13:21 -04:00
|
|
|
import { useGroup } from "./GroupContext";
|
|
|
|
|
|
|
|
import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
|
|
|
|
|
2021-06-09 09:25:28 -04:00
|
|
|
import usePreventSelect from "../hooks/usePreventSelect";
|
|
|
|
|
2021-05-27 23:13:21 -04:00
|
|
|
const TileDragContext = React.createContext();
|
|
|
|
|
|
|
|
export const BASE_SORTABLE_ID = "__base__";
|
|
|
|
export const GROUP_SORTABLE_ID = "__group__";
|
|
|
|
export const GROUP_ID_PREFIX = "__group__";
|
2021-06-08 09:46:20 -04:00
|
|
|
export const UNGROUP_ID = "__ungroup__";
|
|
|
|
export const ADD_TO_MAP_ID = "__add__";
|
|
|
|
|
|
|
|
// Custom rectIntersect that takes a point
|
|
|
|
function rectIntersection(rects, point) {
|
|
|
|
for (let rect of rects) {
|
|
|
|
const [id, bounds] = rect;
|
|
|
|
if (
|
|
|
|
id &&
|
|
|
|
bounds &&
|
|
|
|
point.x > bounds.offsetLeft &&
|
|
|
|
point.x < bounds.offsetLeft + bounds.width &&
|
|
|
|
point.y > bounds.offsetTop &&
|
|
|
|
point.y < bounds.offsetTop + bounds.height
|
|
|
|
) {
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2021-05-27 23:13:21 -04:00
|
|
|
|
2021-06-08 20:33:47 -04:00
|
|
|
export function TileDragProvider({
|
|
|
|
onDragAdd,
|
|
|
|
onDragStart,
|
|
|
|
onDragEnd,
|
|
|
|
onDragCancel,
|
|
|
|
children,
|
|
|
|
}) {
|
2021-05-27 23:13:21 -04:00
|
|
|
const {
|
2021-06-05 02:38:01 -04:00
|
|
|
groups,
|
|
|
|
activeGroups,
|
2021-05-27 23:13:21 -04:00
|
|
|
openGroupId,
|
|
|
|
selectedGroupIds,
|
|
|
|
onGroupsChange,
|
|
|
|
onGroupSelect,
|
|
|
|
onGroupClose,
|
2021-06-05 02:38:01 -04:00
|
|
|
filter,
|
2021-05-27 23:13:21 -04:00
|
|
|
} = useGroup();
|
|
|
|
|
2021-06-09 09:25:28 -04:00
|
|
|
const mouseSensor = useSensor(MouseSensor, {
|
|
|
|
activationConstraint: { delay: 250, tolerance: 5 },
|
|
|
|
});
|
|
|
|
const touchSensor = useSensor(TouchSensor, {
|
2021-05-27 23:13:21 -04:00
|
|
|
activationConstraint: { delay: 250, tolerance: 5 },
|
|
|
|
});
|
2021-06-08 20:33:47 -04:00
|
|
|
const keyboardSensor = useSensor(KeyboardSensor);
|
2021-05-27 23:13:21 -04:00
|
|
|
|
2021-06-09 09:25:28 -04:00
|
|
|
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
2021-05-27 23:13:21 -04:00
|
|
|
|
|
|
|
const [dragId, setDragId] = useState();
|
|
|
|
const [overId, setOverId] = useState();
|
2021-06-04 23:04:56 -04:00
|
|
|
const [dragCursor, setDragCursor] = useState("pointer");
|
2021-05-27 23:13:21 -04:00
|
|
|
|
2021-06-09 09:25:28 -04:00
|
|
|
const [preventSelect, resumeSelect] = usePreventSelect();
|
|
|
|
|
2021-06-08 20:33:47 -04:00
|
|
|
function handleDragStart(event) {
|
|
|
|
const { active, over } = event;
|
2021-05-27 23:13:21 -04:00
|
|
|
setDragId(active.id);
|
|
|
|
setOverId(over?.id);
|
|
|
|
if (!selectedGroupIds.includes(active.id)) {
|
|
|
|
onGroupSelect(active.id);
|
|
|
|
}
|
2021-06-04 23:04:56 -04:00
|
|
|
setDragCursor("grabbing");
|
2021-06-08 20:33:47 -04:00
|
|
|
|
|
|
|
onDragStart && onDragStart(event);
|
2021-06-09 09:25:28 -04:00
|
|
|
|
|
|
|
preventSelect();
|
2021-05-27 23:13:21 -04:00
|
|
|
}
|
|
|
|
|
2021-06-08 20:33:47 -04:00
|
|
|
function handleDragOver(event) {
|
|
|
|
const { over } = event;
|
|
|
|
|
2021-05-27 23:13:21 -04:00
|
|
|
setOverId(over?.id);
|
2021-05-28 03:06:20 -04:00
|
|
|
if (over) {
|
2021-06-04 23:04:56 -04:00
|
|
|
if (
|
2021-06-08 09:46:20 -04:00
|
|
|
over.id.startsWith(UNGROUP_ID) ||
|
2021-06-04 23:04:56 -04:00
|
|
|
over.id.startsWith(GROUP_ID_PREFIX)
|
|
|
|
) {
|
2021-05-28 03:06:20 -04:00
|
|
|
setDragCursor("alias");
|
2021-06-08 09:46:20 -04:00
|
|
|
} else if (over.id.startsWith(ADD_TO_MAP_ID)) {
|
2021-06-02 22:25:23 -04:00
|
|
|
setDragCursor(onDragAdd ? "copy" : "no-drop");
|
2021-05-28 03:06:20 -04:00
|
|
|
} else {
|
2021-06-02 22:25:23 -04:00
|
|
|
setDragCursor("grabbing");
|
2021-05-28 03:06:20 -04:00
|
|
|
}
|
|
|
|
}
|
2021-05-27 23:13:21 -04:00
|
|
|
}
|
|
|
|
|
2021-06-08 20:33:47 -04:00
|
|
|
function handleDragEnd(event) {
|
2021-06-10 04:04:32 -04:00
|
|
|
const { active, over, overlayNodeClientRect } = event;
|
2021-06-08 20:33:47 -04:00
|
|
|
|
2021-05-27 23:13:21 -04:00
|
|
|
setDragId();
|
|
|
|
setOverId();
|
2021-06-04 23:04:56 -04:00
|
|
|
setDragCursor("pointer");
|
2021-06-08 20:33:47 -04:00
|
|
|
if (active && over && active.id !== over.id) {
|
|
|
|
let selectedIndices = selectedGroupIds.map((groupId) =>
|
|
|
|
activeGroups.findIndex((group) => group.id === groupId)
|
2021-05-27 23:13:21 -04:00
|
|
|
);
|
2021-06-08 20:33:47 -04:00
|
|
|
// Maintain current group sorting
|
|
|
|
selectedIndices = selectedIndices.sort((a, b) => a - b);
|
|
|
|
|
|
|
|
if (over.id.startsWith(GROUP_ID_PREFIX)) {
|
|
|
|
onGroupSelect();
|
|
|
|
// Handle tile group
|
|
|
|
const overId = over.id.slice(9);
|
|
|
|
if (overId !== active.id) {
|
|
|
|
const overGroupIndex = activeGroups.findIndex(
|
|
|
|
(group) => group.id === overId
|
|
|
|
);
|
|
|
|
onGroupsChange(
|
|
|
|
moveGroupsInto(activeGroups, overGroupIndex, selectedIndices),
|
|
|
|
openGroupId
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else if (over.id === UNGROUP_ID) {
|
|
|
|
onGroupSelect();
|
|
|
|
// Handle tile ungroup
|
|
|
|
const newGroups = ungroup(groups, openGroupId, selectedIndices);
|
|
|
|
// Close group if it was removed
|
|
|
|
if (!newGroups.find((group) => group.id === openGroupId)) {
|
|
|
|
onGroupClose();
|
|
|
|
}
|
|
|
|
onGroupsChange(newGroups);
|
|
|
|
} else if (over.id === ADD_TO_MAP_ID) {
|
2021-06-10 04:04:32 -04:00
|
|
|
onDragAdd &&
|
|
|
|
overlayNodeClientRect &&
|
|
|
|
onDragAdd(selectedGroupIds, overlayNodeClientRect);
|
2021-06-08 20:33:47 -04:00
|
|
|
} else if (!filter) {
|
|
|
|
// Hanlde tile move only if we have no filter
|
|
|
|
const overGroupIndex = activeGroups.findIndex(
|
|
|
|
(group) => group.id === over.id
|
|
|
|
);
|
|
|
|
onGroupsChange(
|
|
|
|
moveGroups(activeGroups, overGroupIndex, selectedIndices),
|
|
|
|
openGroupId
|
|
|
|
);
|
2021-05-27 23:13:21 -04:00
|
|
|
}
|
|
|
|
}
|
2021-06-08 20:33:47 -04:00
|
|
|
|
2021-06-09 09:25:28 -04:00
|
|
|
resumeSelect();
|
|
|
|
|
2021-06-08 20:33:47 -04:00
|
|
|
onDragEnd && onDragEnd(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleDragCancel(event) {
|
|
|
|
setDragId();
|
|
|
|
setOverId();
|
|
|
|
setDragCursor("pointer");
|
|
|
|
|
2021-06-09 09:25:28 -04:00
|
|
|
resumeSelect();
|
|
|
|
|
2021-06-08 20:33:47 -04:00
|
|
|
onDragCancel && onDragCancel(event);
|
2021-05-27 23:13:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function customCollisionDetection(rects, rect) {
|
2021-06-08 09:46:20 -04:00
|
|
|
const rectCenter = {
|
|
|
|
x: rect.left + rect.width / 2,
|
2021-06-08 09:53:27 -04:00
|
|
|
y: rect.top + rect.height / 2,
|
2021-06-08 09:46:20 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
// Find whether out rect center is outside our add to map rect
|
|
|
|
const addRect = rects.find(([id]) => id === ADD_TO_MAP_ID);
|
|
|
|
if (addRect) {
|
|
|
|
const intersectingAddRect = rectIntersection([addRect], rectCenter);
|
|
|
|
if (!intersectingAddRect) {
|
|
|
|
return ADD_TO_MAP_ID;
|
2021-05-27 23:13:21 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-08 09:46:20 -04:00
|
|
|
// Find whether out rect center is outside our ungroup rect
|
|
|
|
if (openGroupId) {
|
|
|
|
const ungroupRect = rects.find(([id]) => id === UNGROUP_ID);
|
|
|
|
if (ungroupRect) {
|
|
|
|
const intersectingGroupRect = rectIntersection(
|
|
|
|
[ungroupRect],
|
|
|
|
rectCenter
|
|
|
|
);
|
|
|
|
if (!intersectingGroupRect) {
|
|
|
|
return UNGROUP_ID;
|
|
|
|
}
|
|
|
|
}
|
2021-05-28 03:06:20 -04:00
|
|
|
}
|
|
|
|
|
2021-06-08 09:46:20 -04:00
|
|
|
const otherRects = rects.filter(
|
|
|
|
([id]) => id !== ADD_TO_MAP_ID && id !== UNGROUP_ID
|
|
|
|
);
|
2021-05-27 23:13:21 -04:00
|
|
|
|
|
|
|
return closestCenter(otherRects, rect);
|
|
|
|
}
|
|
|
|
|
2021-05-28 03:06:20 -04:00
|
|
|
const value = { dragId, overId, dragCursor };
|
2021-05-27 23:13:21 -04:00
|
|
|
|
|
|
|
return (
|
2021-06-10 04:04:32 -04:00
|
|
|
<DragContext
|
2021-05-27 23:13:21 -04:00
|
|
|
onDragStart={handleDragStart}
|
|
|
|
onDragEnd={handleDragEnd}
|
|
|
|
onDragOver={handleDragOver}
|
2021-06-08 20:33:47 -04:00
|
|
|
onDragCancel={handleDragCancel}
|
2021-05-27 23:13:21 -04:00
|
|
|
sensors={sensors}
|
|
|
|
collisionDetection={customCollisionDetection}
|
|
|
|
>
|
|
|
|
<TileDragContext.Provider value={value}>
|
|
|
|
{children}
|
|
|
|
</TileDragContext.Provider>
|
2021-06-10 04:04:32 -04:00
|
|
|
</DragContext>
|
2021-05-27 23:13:21 -04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useTileDrag() {
|
|
|
|
const context = useContext(TileDragContext);
|
|
|
|
if (context === undefined) {
|
|
|
|
throw new Error("useTileDrag must be used within a TileDragProvider");
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default TileDragContext;
|