diff --git a/src/components/drag/Droppable.js b/src/components/drag/Droppable.js
index facc108..d11d00f 100644
--- a/src/components/drag/Droppable.js
+++ b/src/components/drag/Droppable.js
@@ -1,10 +1,14 @@
import React from "react";
import { useDroppable } from "@dnd-kit/core";
-function Droppable({ id, children, disabled }) {
+function Droppable({ id, children, disabled, ...props }) {
const { setNodeRef } = useDroppable({ id, disabled });
- return
{children}
;
+ return (
+
+ {children}
+
+ );
}
Droppable.defaultProps = {
diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js
index 4cb4be3..294661d 100644
--- a/src/components/map/MapTiles.js
+++ b/src/components/map/MapTiles.js
@@ -9,15 +9,11 @@ import { getGroupItems } from "../../helpers/group";
import { useGroup } from "../../contexts/GroupContext";
-function MapTiles({ maps, onMapEdit, onMapSelect, subgroup, columns }) {
+function MapTiles({ maps, onMapEdit, onMapSelect, subgroup }) {
const {
- groups,
selectedGroupIds,
- openGroupId,
- openGroupItems,
selectMode,
onGroupOpen,
- onGroupsChange,
onGroupSelect,
} = useGroup();
@@ -57,17 +53,7 @@ function MapTiles({ maps, onMapEdit, onMapSelect, subgroup, columns }) {
}
}
- return (
-
- );
+ return ;
}
export default MapTiles;
diff --git a/src/components/tile/SortableTile.js b/src/components/tile/SortableTile.js
index 87f8363..1b2e15f 100644
--- a/src/components/tile/SortableTile.js
+++ b/src/components/tile/SortableTile.js
@@ -4,7 +4,16 @@ import { useDroppable } from "@dnd-kit/core";
import { useSortable } from "@dnd-kit/sortable";
import { animated, useSpring } from "react-spring";
-function SortableTile({ id, disableGrouping, hidden, children, isDragging }) {
+import { GROUP_ID_PREFIX } from "../../contexts/TileDragContext";
+
+function SortableTile({
+ id,
+ disableGrouping,
+ disableSorting,
+ hidden,
+ children,
+ isDragging,
+}) {
const {
attributes,
listeners,
@@ -12,9 +21,10 @@ function SortableTile({ id, disableGrouping, hidden, children, isDragging }) {
setDraggableNodeRef,
over,
active,
- } = useSortable({ id });
+ } = useSortable({ id, disabled: disableSorting });
+
const { setNodeRef: setGroupNodeRef } = useDroppable({
- id: `__group__${id}`,
+ id: `${GROUP_ID_PREFIX}${id}`,
disabled: disableGrouping,
});
@@ -44,7 +54,9 @@ function SortableTile({ id, disableGrouping, hidden, children, isDragging }) {
borderWidth: "4px",
borderRadius: "4px",
borderStyle:
- over?.id === `__group__${id}` && active.id !== id ? "solid" : "none",
+ over?.id === `${GROUP_ID_PREFIX}${id}` && active.id !== id
+ ? "solid"
+ : "none",
};
const { opacity } = useSpring({ opacity: hidden ? 0 : 1 });
diff --git a/src/components/tile/SortableTiles.js b/src/components/tile/SortableTiles.js
index 0ced96b..ad575af 100644
--- a/src/components/tile/SortableTiles.js
+++ b/src/components/tile/SortableTiles.js
@@ -1,88 +1,43 @@
-import React, { useState } from "react";
+import React from "react";
import { createPortal } from "react-dom";
-import {
- DndContext,
- DragOverlay,
- MouseSensor,
- TouchSensor,
- useSensor,
- useSensors,
- closestCenter,
-} from "@dnd-kit/core";
+import { DragOverlay } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable";
import { animated, useSpring, config } from "react-spring";
import { Badge } from "theme-ui";
-import { moveGroupsInto, moveGroups } from "../../helpers/group";
+import { moveGroupsInto } from "../../helpers/group";
import { keyBy } from "../../helpers/shared";
import Vector2 from "../../helpers/Vector2";
import SortableTile from "./SortableTile";
-function SortableTiles({
- groups,
- selectedGroupIds,
- onGroupChange,
- renderTile,
- onTileSelect,
- disableGrouping,
- openGroupId,
-}) {
- const mouseSensor = useSensor(MouseSensor, {
- activationConstraint: { delay: 250, tolerance: 5 },
- });
- const touchSensor = useSensor(TouchSensor, {
- activationConstraint: { delay: 250, tolerance: 5 },
- });
+import {
+ useTileDrag,
+ BASE_SORTABLE_ID,
+ GROUP_SORTABLE_ID,
+ GROUP_ID_PREFIX,
+} from "../../contexts/TileDragContext";
+import { useGroup } from "../../contexts/GroupContext";
- const sensors = useSensors(mouseSensor, touchSensor);
+function SortableTiles({ renderTile, subgroup }) {
+ const { dragId, overId } = useTileDrag();
+ const {
+ groups: allGroups,
+ selectedGroupIds: allSelectedIds,
+ openGroupId,
+ openGroupItems,
+ } = useGroup();
- const [dragId, setDragId] = useState();
- const [overId, setOverId] = useState();
+ const sortableId = subgroup ? GROUP_SORTABLE_ID : BASE_SORTABLE_ID;
- function handleDragStart({ active, over }) {
- setDragId(active.id);
- setOverId(over?.id);
- if (!selectedGroupIds.includes(active.id)) {
- onTileSelect(active.id);
- }
- }
-
- function handleDragOver({ over }) {
- setOverId(over?.id);
- }
-
- function handleDragEnd({ active, over }) {
- setDragId();
- setOverId();
- if (!active || !over) {
- return;
- }
-
- let selectedIndices = selectedGroupIds.map((groupId) =>
- groups.findIndex((group) => group.id === groupId)
- );
- // Maintain current group sorting
- selectedIndices = selectedIndices.sort((a, b) => a - b);
-
- if (over.id.startsWith("__group__")) {
- const overId = over.id.slice(9);
- if (overId === active.id) {
- return;
- }
-
- const overGroupIndex = groups.findIndex((group) => group.id === overId);
- onGroupChange(moveGroupsInto(groups, overGroupIndex, selectedIndices));
- onTileSelect();
- } else {
- if (active.id === over.id) {
- return;
- }
-
- const overGroupIndex = groups.findIndex((group) => group.id === over.id);
- onGroupChange(moveGroups(groups, overGroupIndex, selectedIndices));
- }
+ const groups = subgroup ? openGroupItems : allGroups;
+ // Only populate selected groups if needed
+ let selectedGroupIds = [];
+ if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
+ selectedGroupIds = allSelectedIds;
}
+ const disableSorting = openGroupId && !subgroup;
+ const disableGrouping = subgroup || disableSorting;
const dragBounce = useSpring({
transform: !!dragId ? "scale(0.9)" : "scale(1)",
@@ -91,7 +46,7 @@ function SortableTiles({
});
const overGroupId =
- overId && overId.startsWith("__group__") && overId.slice(9);
+ overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9);
function renderSortableGroup(group, selectedGroups) {
if (overGroupId === group.id && dragId && group.id !== dragId) {
@@ -179,6 +134,7 @@ function SortableTiles({
id={group.id}
key={group.id}
disableGrouping={disableTileGrouping}
+ disableSorting={disableSorting}
hidden={group.id === openGroupId}
isDragging={isDragging}
>
@@ -189,18 +145,10 @@ function SortableTiles({
}
return (
-
-
- {renderTiles()}
- {createPortal(dragId && renderDragOverlays(), document.body)}
-
-
+
+ {renderTiles()}
+ {createPortal(dragId && renderDragOverlays(), document.body)}
+
);
}
diff --git a/src/components/tile/TilesOverlay.js b/src/components/tile/TilesOverlay.js
index 27b072e..cb15c8a 100644
--- a/src/components/tile/TilesOverlay.js
+++ b/src/components/tile/TilesOverlay.js
@@ -1,10 +1,14 @@
import React, { useState } from "react";
+import { createPortal } from "react-dom";
import { Box, Close, Grid, useThemeUI } from "theme-ui";
import { useSpring, animated, config } from "react-spring";
import ReactResizeDetector from "react-resize-detector";
import SimpleBar from "simplebar-react";
import { useGroup } from "../../contexts/GroupContext";
+import { UNGROUP_ID_PREFIX } from "../../contexts/TileDragContext";
+
+import Droppable from "../drag/Droppable";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
@@ -27,8 +31,55 @@ function TilesOverlay({ children }) {
setContinerSize(size);
}
+ function renderUngroupBoxes() {
+ return createPortal(
+
+
+
+
+
+
,
+ document.body
+ );
+ }
+
return (
<>
+ {openGroupId && renderUngroupBoxes()}
{openGroupId && (
- );
+ return ;
}
export default TokenTiles;
diff --git a/src/contexts/GroupContext.js b/src/contexts/GroupContext.js
index 7fdd97a..b414e75 100644
--- a/src/contexts/GroupContext.js
+++ b/src/contexts/GroupContext.js
@@ -40,10 +40,13 @@ export function GroupProvider({
setOpenGroupId();
}
- function handleGroupsChange(newGroups) {
- if (openGroupId) {
- // If a group is open then update that group with the new items
- const groupIndex = groups.findIndex((group) => group.id === openGroupId);
+ /**
+ * @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object
+ */
+ function handleGroupsChange(newGroups, groupId) {
+ if (groupId) {
+ // If a group is specidifed then update that group with the new items
+ const groupIndex = groups.findIndex((group) => group.id === groupId);
let updatedGroups = cloneDeep(groups);
const group = updatedGroups[groupIndex];
updatedGroups[groupIndex] = { ...group, items: newGroups };
diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.js
new file mode 100644
index 0000000..4155152
--- /dev/null
+++ b/src/contexts/TileDragContext.js
@@ -0,0 +1,148 @@
+import React, { useState, useContext } from "react";
+import {
+ DndContext,
+ MouseSensor,
+ TouchSensor,
+ useSensor,
+ useSensors,
+ closestCenter,
+ rectIntersection,
+} from "@dnd-kit/core";
+
+import { useGroup } from "./GroupContext";
+
+import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
+
+const TileDragContext = React.createContext();
+
+export const BASE_SORTABLE_ID = "__base__";
+export const GROUP_SORTABLE_ID = "__group__";
+export const GROUP_ID_PREFIX = "__group__";
+export const UNGROUP_ID_PREFIX = "__ungroup__";
+
+export function TileDragProvider({ children }) {
+ const {
+ groups: allGroups,
+ openGroupId,
+ openGroupItems,
+ selectedGroupIds,
+ onGroupsChange,
+ onGroupSelect,
+ onGroupClose,
+ } = useGroup();
+
+ const groupOpen = !!openGroupId;
+
+ const groups = groupOpen ? openGroupItems : allGroups;
+
+ const mouseSensor = useSensor(MouseSensor, {
+ activationConstraint: { delay: 250, tolerance: 5 },
+ });
+ const touchSensor = useSensor(TouchSensor, {
+ activationConstraint: { delay: 250, tolerance: 5 },
+ });
+
+ const sensors = useSensors(mouseSensor, touchSensor);
+
+ const [dragId, setDragId] = useState();
+ const [overId, setOverId] = useState();
+
+ function handleDragStart({ active, over }) {
+ setDragId(active.id);
+ setOverId(over?.id);
+ if (!selectedGroupIds.includes(active.id)) {
+ onGroupSelect(active.id);
+ }
+ }
+
+ function handleDragOver({ over }) {
+ setOverId(over?.id);
+ }
+
+ function handleDragEnd({ active, over }) {
+ setDragId();
+ setOverId();
+ if (!active || !over || active.id === over.id) {
+ return;
+ }
+
+ let selectedIndices = selectedGroupIds.map((groupId) =>
+ groups.findIndex((group) => group.id === groupId)
+ );
+ // Maintain current group sorting
+ selectedIndices = selectedIndices.sort((a, b) => a - b);
+
+ if (over.id.startsWith(GROUP_ID_PREFIX)) {
+ // Handle tile group
+ const overId = over.id.slice(9);
+ if (overId === active.id) {
+ return;
+ }
+
+ const overGroupIndex = groups.findIndex((group) => group.id === overId);
+ onGroupsChange(
+ moveGroupsInto(groups, overGroupIndex, selectedIndices),
+ openGroupId
+ );
+ onGroupSelect();
+ } else if (over.id.startsWith(UNGROUP_ID_PREFIX)) {
+ // Handle tile ungroup
+ const newGroups = ungroup(allGroups, openGroupId, selectedIndices);
+ // Close group if it was removed
+ if (!newGroups.find((group) => group.id === openGroupId)) {
+ onGroupClose();
+ }
+ onGroupsChange(newGroups);
+ onGroupSelect();
+ } else {
+ // Hanlde tile move
+ const overGroupIndex = groups.findIndex((group) => group.id === over.id);
+ onGroupsChange(
+ moveGroups(groups, overGroupIndex, selectedIndices),
+ openGroupId
+ );
+ }
+ }
+
+ function customCollisionDetection(rects, rect) {
+ if (groupOpen) {
+ const ungroupRects = rects.filter(([id]) =>
+ id.startsWith(UNGROUP_ID_PREFIX)
+ );
+ const intersectingGroupRect = rectIntersection(ungroupRects, rect);
+ if (intersectingGroupRect) {
+ return intersectingGroupRect;
+ }
+ }
+
+ const otherRects = rects.filter(([id]) => id !== UNGROUP_ID_PREFIX);
+
+ return closestCenter(otherRects, rect);
+ }
+
+ const value = { dragId, overId };
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+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;
diff --git a/src/helpers/group.js b/src/helpers/group.js
index d6a86f0..f474d9d 100644
--- a/src/helpers/group.js
+++ b/src/helpers/group.js
@@ -156,6 +156,39 @@ export function moveGroups(groups, to, indices) {
return newGroups;
}
+/**
+ * Move items from a sub group to the start of the base group
+ * @param {Group[]} groups
+ * @param {string} fromId The id of the group to move from
+ * @param {number[]} indices The indices of the items in the group
+ */
+export function ungroup(groups, fromId, indices) {
+ const newGroups = cloneDeep(groups);
+
+ let fromIndex = newGroups.findIndex((group) => group.id === fromId);
+
+ let items = [];
+ for (let i of indices) {
+ items.push(newGroups[fromIndex].items[i]);
+ }
+
+ // Remove items from previous group
+ for (let item of items) {
+ const i = newGroups[fromIndex].items.findIndex((el) => el.id === item.id);
+ newGroups[fromIndex].items.splice(i, 1);
+ }
+
+ // If we have no more items in the group delete it
+ if (newGroups[fromIndex].items.length === 0) {
+ newGroups.splice(fromIndex, 1);
+ }
+
+ // Add to base group
+ newGroups.splice(0, 0, ...items);
+
+ return newGroups;
+}
+
/**
* Recursively find a group within a group array
* @param {Group[]} groups
diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js
index 00e3d92..7fbf618 100644
--- a/src/modals/SelectMapModal.js
+++ b/src/modals/SelectMapModal.js
@@ -24,6 +24,7 @@ import { useAuth } from "../contexts/AuthContext";
import { useKeyboard } from "../contexts/KeyboardContext";
import { useAssets } from "../contexts/AssetsContext";
import { GroupProvider } from "../contexts/GroupContext";
+import { TileDragProvider } from "../contexts/TileDragContext";
import shortcuts from "../shortcuts";
@@ -259,21 +260,25 @@ function SelectMapModal({
onGroupsSelect={setSelectedGroupIds}
disabled={!isOpen}
>
-
- setIsEditModalOpen(true)}
- onMapSelect={handleMapSelect}
- />
-
-
- setIsEditModalOpen(true)}
- onMapSelect={handleMapSelect}
- subgroup
- />
-
+
+
+ setIsEditModalOpen(true)}
+ onMapSelect={handleMapSelect}
+ />
+
+
+
+
+ setIsEditModalOpen(true)}
+ onMapSelect={handleMapSelect}
+ subgroup
+ />
+
+