From 57aafce9383326da775820e7f1c68f72fb9fb251 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 28 May 2021 13:13:21 +1000 Subject: [PATCH] Add support for ungrouping tiles and refactor tile drag functions --- src/components/drag/Droppable.js | 8 +- src/components/map/MapTiles.js | 18 +--- src/components/tile/SortableTile.js | 20 +++- src/components/tile/SortableTiles.js | 116 ++++++--------------- src/components/tile/TilesOverlay.js | 51 +++++++++ src/components/token/TokenTiles.js | 16 +-- src/contexts/GroupContext.js | 11 +- src/contexts/TileDragContext.js | 148 +++++++++++++++++++++++++++ src/helpers/group.js | 33 ++++++ src/modals/SelectMapModal.js | 35 ++++--- src/modals/SelectTokensModal.js | 31 +++--- 11 files changed, 334 insertions(+), 153 deletions(-) create mode 100644 src/contexts/TileDragContext.js 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 + /> + +