2021-06-15 06:08:45 -04:00
|
|
|
import React, { useState, useContext, useEffect } from "react";
|
2021-05-27 23:13:21 -04:00
|
|
|
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,
|
2021-07-09 02:22:35 -04:00
|
|
|
RectEntry,
|
2021-05-27 23:13:21 -04:00
|
|
|
} from "@dnd-kit/core";
|
|
|
|
|
2021-07-09 02:22:35 -04:00
|
|
|
import DragContext, { CustomDragEndEvent } from "./DragContext";
|
|
|
|
import { DragStartEvent, DragOverEvent, ViewRect } from "@dnd-kit/core";
|
|
|
|
import { DragCancelEvent } from "@dnd-kit/core/dist/types";
|
2021-06-10 04:04:32 -04:00
|
|
|
|
2021-05-27 23:13:21 -04:00
|
|
|
import { useGroup } from "./GroupContext";
|
|
|
|
|
|
|
|
import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
|
2021-07-09 02:22:35 -04:00
|
|
|
import Vector2 from "../helpers/Vector2";
|
2021-05-27 23:13:21 -04:00
|
|
|
|
2021-06-09 09:25:28 -04:00
|
|
|
import usePreventSelect from "../hooks/usePreventSelect";
|
2021-07-16 00:55:33 -04:00
|
|
|
import { GroupItem } from "../types/Group";
|
2021-06-09 09:25:28 -04:00
|
|
|
|
2021-07-09 02:22:35 -04:00
|
|
|
const TileDragIdContext =
|
|
|
|
React.createContext<string | undefined | null>(undefined);
|
|
|
|
const TileOverGroupIdContext =
|
|
|
|
React.createContext<string | undefined | null>(undefined);
|
|
|
|
const TileDragCursorContext =
|
|
|
|
React.createContext<string | undefined | null>(undefined);
|
2021-05-27 23:13:21 -04:00
|
|
|
|
|
|
|
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
|
2021-07-09 02:22:35 -04:00
|
|
|
function rectIntersection(rects: RectEntry[], point: Vector2) {
|
2021-06-08 09:46:20 -04:00
|
|
|
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-07-09 02:22:35 -04:00
|
|
|
type TileDragProviderProps = {
|
|
|
|
onDragAdd?: (selectedGroupIds: string[], rect: DOMRect) => void;
|
|
|
|
onDragStart?: (event: DragStartEvent) => void;
|
|
|
|
onDragEnd?: (event: CustomDragEndEvent) => void;
|
|
|
|
onDragCancel?: (event: DragCancelEvent) => void;
|
|
|
|
children?: React.ReactNode;
|
|
|
|
};
|
|
|
|
|
2021-06-08 20:33:47 -04:00
|
|
|
export function TileDragProvider({
|
|
|
|
onDragAdd,
|
|
|
|
onDragStart,
|
|
|
|
onDragEnd,
|
|
|
|
onDragCancel,
|
|
|
|
children,
|
2021-07-09 02:22:35 -04:00
|
|
|
}: TileDragProviderProps) {
|
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,
|
2021-07-16 00:55:33 -04:00
|
|
|
onSubgroupChange,
|
2021-05-27 23:13:21 -04:00
|
|
|
onGroupSelect,
|
2021-07-16 00:55:33 -04:00
|
|
|
onClearSelection,
|
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, {
|
2021-06-15 06:08:45 -04:00
|
|
|
activationConstraint: { distance: 3 },
|
2021-06-09 09:25:28 -04:00
|
|
|
});
|
|
|
|
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
|
|
|
|
2021-07-09 02:22:35 -04:00
|
|
|
const [dragId, setDragId] = useState<string | null>(null);
|
|
|
|
const [overId, setOverId] = useState<string | null>(null);
|
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-07-09 02:22:35 -04:00
|
|
|
const [overGroupId, setOverGroupId] = useState<string | null>(null);
|
2021-06-15 06:08:45 -04:00
|
|
|
useEffect(() => {
|
|
|
|
setOverGroupId(
|
|
|
|
(overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9)) || null
|
|
|
|
);
|
|
|
|
}, [overId]);
|
|
|
|
|
2021-07-09 02:22:35 -04:00
|
|
|
function handleDragStart(event: DragStartEvent) {
|
|
|
|
const { active } = event;
|
2021-05-27 23:13:21 -04:00
|
|
|
setDragId(active.id);
|
2021-07-09 02:22:35 -04:00
|
|
|
setOverId(null);
|
2021-05-27 23:13:21 -04:00
|
|
|
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-07-09 02:22:35 -04:00
|
|
|
function handleDragOver(event: DragOverEvent) {
|
2021-06-08 20:33:47 -04:00
|
|
|
const { over } = event;
|
|
|
|
|
2021-06-15 06:08:45 -04:00
|
|
|
setOverId(over?.id || null);
|
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-07-09 02:22:35 -04:00
|
|
|
function handleDragEnd(event: CustomDragEndEvent) {
|
2021-06-10 04:04:32 -04:00
|
|
|
const { active, over, overlayNodeClientRect } = event;
|
2021-06-08 20:33:47 -04:00
|
|
|
|
2021-06-15 06:08:45 -04:00
|
|
|
setDragId(null);
|
|
|
|
setOverId(null);
|
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)) {
|
2021-07-16 00:55:33 -04:00
|
|
|
onClearSelection();
|
2021-06-08 20:33:47 -04:00
|
|
|
// Handle tile group
|
|
|
|
const overId = over.id.slice(9);
|
|
|
|
if (overId !== active.id) {
|
|
|
|
const overGroupIndex = activeGroups.findIndex(
|
|
|
|
(group) => group.id === overId
|
|
|
|
);
|
2021-07-16 00:55:33 -04:00
|
|
|
const newGroups = moveGroupsInto(
|
|
|
|
activeGroups,
|
|
|
|
overGroupIndex,
|
|
|
|
selectedIndices
|
2021-06-08 20:33:47 -04:00
|
|
|
);
|
2021-07-16 00:55:33 -04:00
|
|
|
if (!openGroupId) {
|
|
|
|
onGroupsChange(newGroups);
|
|
|
|
}
|
2021-06-08 20:33:47 -04:00
|
|
|
}
|
|
|
|
} else if (over.id === UNGROUP_ID) {
|
2021-07-09 02:22:35 -04:00
|
|
|
if (openGroupId) {
|
2021-07-16 00:55:33 -04:00
|
|
|
onClearSelection();
|
2021-07-09 02:22:35 -04:00
|
|
|
// Handle tile ungroup
|
|
|
|
const newGroups = ungroup(groups, openGroupId, selectedIndices);
|
2021-07-16 00:55:33 -04:00
|
|
|
onGroupsChange(newGroups);
|
2021-07-09 02:22:35 -04:00
|
|
|
}
|
2021-06-08 20:33:47 -04:00
|
|
|
} 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
|
|
|
|
);
|
2021-07-16 00:55:33 -04:00
|
|
|
const newGroups = moveGroups(
|
|
|
|
activeGroups,
|
|
|
|
overGroupIndex,
|
|
|
|
selectedIndices
|
2021-06-08 20:33:47 -04:00
|
|
|
);
|
2021-07-16 00:55:33 -04:00
|
|
|
if (openGroupId) {
|
|
|
|
onSubgroupChange(newGroups as GroupItem[], openGroupId);
|
|
|
|
} else {
|
|
|
|
onGroupsChange(newGroups);
|
|
|
|
}
|
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);
|
|
|
|
}
|
|
|
|
|
2021-07-09 02:22:35 -04:00
|
|
|
function handleDragCancel(event: DragCancelEvent) {
|
2021-06-15 06:08:45 -04:00
|
|
|
setDragId(null);
|
|
|
|
setOverId(null);
|
2021-06-08 20:33:47 -04:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-07-09 02:22:35 -04:00
|
|
|
function customCollisionDetection(rects: RectEntry[], rect: ViewRect) {
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
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}
|
|
|
|
>
|
2021-06-15 06:08:45 -04:00
|
|
|
<TileDragIdContext.Provider value={dragId}>
|
|
|
|
<TileOverGroupIdContext.Provider value={overGroupId}>
|
|
|
|
<TileDragCursorContext.Provider value={dragCursor}>
|
|
|
|
{children}
|
|
|
|
</TileDragCursorContext.Provider>
|
|
|
|
</TileOverGroupIdContext.Provider>
|
|
|
|
</TileDragIdContext.Provider>
|
2021-06-10 04:04:32 -04:00
|
|
|
</DragContext>
|
2021-05-27 23:13:21 -04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-06-15 06:08:45 -04:00
|
|
|
export function useTileDragId() {
|
|
|
|
const context = useContext(TileDragIdContext);
|
|
|
|
if (context === undefined) {
|
|
|
|
throw new Error("useTileDrag must be used within a TileDragProvider");
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useTileOverGroupId() {
|
|
|
|
const context = useContext(TileOverGroupIdContext);
|
2021-05-27 23:13:21 -04:00
|
|
|
if (context === undefined) {
|
|
|
|
throw new Error("useTileDrag must be used within a TileDragProvider");
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|
|
|
|
|
2021-06-15 06:08:45 -04:00
|
|
|
export function useTileDragCursor() {
|
|
|
|
const context = useContext(TileDragCursorContext);
|
|
|
|
if (context === undefined) {
|
|
|
|
throw new Error("useTileDrag must be used within a TileDragProvider");
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|