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,
+ };
+ }
+}