grungnet/src/contexts/TileDragContext.tsx

290 lines
8.0 KiB
TypeScript
Raw Normal View History

import React, { useState, useContext, useEffect } from "react";
import {
MouseSensor,
TouchSensor,
2021-06-08 20:33:47 -04:00
KeyboardSensor,
useSensor,
useSensors,
closestCenter,
2021-07-09 02:22:35 -04:00
RectEntry,
} 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";
import { useGroup } from "./GroupContext";
import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
2021-07-09 02:22:35 -04:00
import Vector2 from "../helpers/Vector2";
import usePreventSelect from "../hooks/usePreventSelect";
2021-07-16 00:55:33 -04:00
import { GroupItem } from "../types/Group";
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);
export const BASE_SORTABLE_ID = "__base__";
export const GROUP_SORTABLE_ID = "__group__";
export const GROUP_ID_PREFIX = "__group__";
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) {
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-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) {
const {
2021-06-05 02:38:01 -04:00
groups,
activeGroups,
openGroupId,
selectedGroupIds,
onGroupsChange,
2021-07-16 00:55:33 -04:00
onSubgroupChange,
onGroupSelect,
2021-07-16 00:55:33 -04:00
onClearSelection,
2021-06-05 02:38:01 -04:00
filter,
} = useGroup();
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: { distance: 3 },
});
const touchSensor = useSensor(TouchSensor, {
activationConstraint: { delay: 250, tolerance: 5 },
});
2021-06-08 20:33:47 -04:00
const keyboardSensor = useSensor(KeyboardSensor);
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
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");
const [preventSelect, resumeSelect] = usePreventSelect();
2021-07-09 02:22:35 -04:00
const [overGroupId, setOverGroupId] = useState<string | null>(null);
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;
setDragId(active.id);
2021-07-09 02:22:35 -04:00
setOverId(null);
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);
preventSelect();
}
2021-07-09 02:22:35 -04:00
function handleDragOver(event: DragOverEvent) {
2021-06-08 20:33:47 -04:00
const { over } = event;
setOverId(over?.id || null);
if (over) {
2021-06-04 23:04:56 -04:00
if (
over.id.startsWith(UNGROUP_ID) ||
2021-06-04 23:04:56 -04:00
over.id.startsWith(GROUP_ID_PREFIX)
) {
setDragCursor("alias");
} else if (over.id.startsWith(ADD_TO_MAP_ID)) {
setDragCursor(onDragAdd ? "copy" : "no-drop");
} else {
setDragCursor("grabbing");
}
}
}
2021-07-09 02:22:35 -04:00
function handleDragEnd(event: CustomDragEndEvent) {
const { active, over, overlayNodeClientRect } = event;
2021-06-08 20:33:47 -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-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) {
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-06-08 20:33:47 -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) {
setDragId(null);
setOverId(null);
2021-06-08 20:33:47 -04:00
setDragCursor("pointer");
resumeSelect();
2021-06-08 20:33:47 -04:00
onDragCancel && onDragCancel(event);
}
2021-07-09 02:22:35 -04:00
function customCollisionDetection(rects: RectEntry[], rect: ViewRect) {
const rectCenter = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
// 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;
}
}
// 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;
}
}
}
const otherRects = rects.filter(
([id]) => id !== ADD_TO_MAP_ID && id !== UNGROUP_ID
);
return closestCenter(otherRects, rect);
}
return (
<DragContext
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
2021-06-08 20:33:47 -04:00
onDragCancel={handleDragCancel}
sensors={sensors}
collisionDetection={customCollisionDetection}
>
<TileDragIdContext.Provider value={dragId}>
<TileOverGroupIdContext.Provider value={overGroupId}>
<TileDragCursorContext.Provider value={dragCursor}>
{children}
</TileDragCursorContext.Provider>
</TileOverGroupIdContext.Provider>
</TileDragIdContext.Provider>
</DragContext>
);
}
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);
if (context === undefined) {
throw new Error("useTileDrag must be used within a TileDragProvider");
}
return context;
}
export function useTileDragCursor() {
const context = useContext(TileDragCursorContext);
if (context === undefined) {
throw new Error("useTileDrag must be used within a TileDragProvider");
}
return context;
}