diff --git a/src/components/drag/Droppable.js b/src/components/drag/Droppable.js index c51c25e..facc108 100644 --- a/src/components/drag/Droppable.js +++ b/src/components/drag/Droppable.js @@ -4,11 +4,7 @@ import { useDroppable } from "@dnd-kit/core"; function Droppable({ id, children, disabled }) { const { setNodeRef } = useDroppable({ id, disabled }); - return ( -
- {children} -
- ); + return
{children}
; } Droppable.defaultProps = { diff --git a/src/components/drag/Sortable.js b/src/components/drag/Sortable.js deleted file mode 100644 index 24f6783..0000000 --- a/src/components/drag/Sortable.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import { useSortable } from "@dnd-kit/sortable"; - -function Sortable({ id, children }) { - const { - attributes, - listeners, - setNodeRef, - isDragging, - transform, - transition, - } = useSortable({ id }); - - const style = { - cursor: "pointer", - opacity: isDragging ? 0.25 : undefined, - transform: - transform && `translate3d(${transform.x}px, ${transform.y}px, 0px)`, - zIndex: isDragging ? 100 : undefined, - transition, - }; - - return ( -
- {children} -
- ); -} - -export default Sortable; diff --git a/src/components/drag/SortableTile.js b/src/components/drag/SortableTile.js new file mode 100644 index 0000000..b8f3be1 --- /dev/null +++ b/src/components/drag/SortableTile.js @@ -0,0 +1,65 @@ +import React from "react"; +import { Box } from "theme-ui"; +import { useDroppable } from "@dnd-kit/core"; +import { useSortable } from "@dnd-kit/sortable"; + +function Sortable({ id, children, showDropGutter }) { + const { + attributes, + listeners, + isDragging, + transition, + setDroppableNodeRef, + setDraggableNodeRef, + } = useSortable({ id }); + const { setNodeRef: setGroupNodeRef } = useDroppable({ + id: `__group__${id}`, + }); + + const dragStyle = { + cursor: "pointer", + opacity: isDragging ? 0.25 : undefined, + zIndex: isDragging ? 100 : undefined, + transition, + }; + + // Sort div left aligned + const sortDropStyle = { + position: "absolute", + left: "-5px", + top: 0, + width: "2px", + height: "100%", + borderRadius: "2px", + }; + + // Group div center aligned + const groupDropStyle = { + position: "absolute", + top: 0, + left: "50%", + width: "1px", + height: "100%", + }; + + return ( + + + {children} + + + + + ); +} + +export default Sortable; diff --git a/src/components/drag/SortableTiles.js b/src/components/drag/SortableTiles.js index 4527ad8..338113f 100644 --- a/src/components/drag/SortableTiles.js +++ b/src/components/drag/SortableTiles.js @@ -7,11 +7,16 @@ import { TouchSensor, useSensor, useSensors, + closestCenter, } from "@dnd-kit/core"; import { SortableContext, arrayMove } from "@dnd-kit/sortable"; import { animated, useSpring, config } from "react-spring"; -function SortableTiles({ groups, onGroupChange, renderTile, children }) { +import { combineGroups } from "../../helpers/select"; + +import SortableTile from "./SortableTile"; + +function SortableTiles({ groups, onGroupChange, renderTile, renderTiles }) { const mouseSensor = useSensor(MouseSensor, { activationConstraint: { delay: 250, tolerance: 5 }, }); @@ -22,14 +27,29 @@ function SortableTiles({ groups, onGroupChange, renderTile, children }) { const sensors = useSensors(mouseSensor, touchSensor); const [dragId, setDragId] = useState(); + const [overId, setOverId] = useState(); - function handleDragStart({ active }) { + function handleDragStart({ active, over }) { setDragId(active.id); + setOverId(over?.id); + } + + function handleDragMove({ over }) { + setOverId(over?.id); } function handleDragEnd({ active, over }) { setDragId(); - if (active && over && active.id !== over.id) { + setOverId(); + if (!active || !over) { + return; + } + + if (over.id.startsWith("__group__")) { + return; + } + + if (active.id !== over.id) { const oldIndex = groups.findIndex((group) => group.id === active.id); const newIndex = groups.findIndex((group) => group.id === over.id); onGroupChange(arrayMove(groups, oldIndex, newIndex)); @@ -37,18 +57,41 @@ function SortableTiles({ groups, onGroupChange, renderTile, children }) { } const dragBounce = useSpring({ - transform: !!dragId ? "scale(1.05)" : "scale(1)", + transform: !!dragId ? "scale(0.9)" : "scale(1)", config: config.wobbly, }); + const overGroupId = + overId && overId.startsWith("__group__") && overId.slice(9); + return ( - {children} + {renderTiles( + groups.map((group) => ( + + {dragId && overGroupId === group.id && group.id !== dragId + ? // If over a group render a preview of that group + renderTile( + combineGroups( + group, + groups.find((group) => group.id === dragId) + ) + ) + : renderTile(group)} + + )) + )} {createPortal( {dragId && ( diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js index 9c815b2..f630e59 100644 --- a/src/components/map/MapTiles.js +++ b/src/components/map/MapTiles.js @@ -10,7 +10,6 @@ import MapTileGroup from "./MapTileGroup"; import Link from "../Link"; import FilterBar from "../FilterBar"; -import Sortable from "../drag/Sortable"; import SortableTiles from "../drag/SortableTiles"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; @@ -69,7 +68,7 @@ function MapTiles({ setHasMapState(_hasMapState); }, [selectedGroupIds, maps, mapStates, groups]); - function groupToMapTile(group) { + function renderTile(group) { if (group.type === "item") { const map = maps.find((map) => map.id === group.id); const isSelected = selectedGroupIds.includes(group.id); @@ -107,12 +106,8 @@ function MapTiles({ const multipleSelected = selectedGroupIds.length > 1; - return ( - + function renderTiles(tiles) { + return ( onTileSelect()} @@ -142,11 +137,7 @@ function MapTiles({ columns={layout.gridTemplate} onClick={() => onTileSelect()} > - {groups.map((group) => ( - - {groupToMapTile(group)} - - ))} + {tiles} {databaseDisabled && ( @@ -205,7 +196,16 @@ function MapTiles({ )} - + ); + } + + return ( + ); } diff --git a/src/components/token/TokenTiles.js b/src/components/token/TokenTiles.js index 29a70e4..cb3c9d2 100644 --- a/src/components/token/TokenTiles.js +++ b/src/components/token/TokenTiles.js @@ -11,7 +11,6 @@ import TokenTileGroup from "./TokenTileGroup"; import Link from "../Link"; import FilterBar from "../FilterBar"; -import Sortable from "../drag/Sortable"; import SortableTiles from "../drag/SortableTiles"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; @@ -49,7 +48,7 @@ function TokenTiles({ setAllTokensVisisble(selectedTokens.every((token) => !token.hideInSidebar)); }, [selectedGroupIds, tokens, groups]); - function groupToTokenTile(group) { + function renderTile(group) { if (group.type === "item") { const token = tokens.find((token) => token.id === group.id); const isSelected = selectedGroupIds.includes(group.id); @@ -102,12 +101,8 @@ function TokenTiles({ } } - return ( - + function renderTiles(tiles) { + return ( onTileSelect()} @@ -136,11 +131,7 @@ function TokenTiles({ columns={layout.gridTemplate} onClick={() => onTileSelect()} > - {groups.map((group) => ( - - {groupToTokenTile(group)} - - ))} + {tiles} {databaseDisabled && ( @@ -199,7 +190,16 @@ function TokenTiles({ )} - + ); + } + + return ( + ); } diff --git a/src/helpers/select.js b/src/helpers/select.js index 4b577d5..45263fa 100644 --- a/src/helpers/select.js +++ b/src/helpers/select.js @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { v4 as uuid } from "uuid"; import Fuse from "fuse.js"; import { groupBy, keyBy } from "./shared"; @@ -135,6 +136,30 @@ export function handleItemSelect( } } +/** + * @typedef GroupItem + * @property {string} id + * @property {"item"} type + */ + +/** + * @typedef GroupContainer + * @property {string} id + * @property {"group"} type + * @property {GroupItem[]} items + * @property {string} name + */ + +/** + * @typedef {GroupItem|GroupContainer} Group + */ + +/** + * Transform an array of group ids to their groups + * @param {string[]} groupIds + * @param {Group[]} groups + * @return {Group[[]} + */ export function groupsFromIds(groupIds, groups) { const groupsByIds = keyBy(groups, "id"); const filteredGroups = []; @@ -144,6 +169,11 @@ export function groupsFromIds(groupIds, groups) { return filteredGroups; } +/** + * Get all items from a group including all sub groups + * @param {Group} group + * @return {GroupItem[]} + */ function getGroupItems(group) { if (group.type === "group") { let groups = []; @@ -156,6 +186,13 @@ function getGroupItems(group) { } } +/** + * Transform an array of groups into their assosiated items + * @param {Group[]} groups + * @param {any[]} allItems + * @param {string} itemKey + * @returns {any[]} + */ export function itemsFromGroups(groups, allItems, itemKey = "id") { const allItemsById = keyBy(allItems, itemKey); const groupedItems = []; @@ -168,3 +205,28 @@ export function itemsFromGroups(groups, allItems, itemKey = "id") { return groupedItems; } + +/** + * Combine two groups + * @param {Group} a + * @param {Group} b + * @returns {GroupContainer} + */ +export function combineGroups(a, b) { + if (a.type === "item") { + return { + id: uuid(), + type: "group", + items: [a, b], + name: "", + }; + } + if (a.type === "group") { + return { + id: a.id, + type: "group", + items: [...a.items, b], + name: a.name, + }; + } +}