Add support for group visual with sortable tiles
This commit is contained in:
parent
d34cd98fa9
commit
9f5d0b8283
@ -4,11 +4,7 @@ import { useDroppable } from "@dnd-kit/core";
|
||||
function Droppable({ id, children, disabled }) {
|
||||
const { setNodeRef } = useDroppable({ id, disabled });
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%" }} ref={setNodeRef}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div ref={setNodeRef}>{children}</div>;
|
||||
}
|
||||
|
||||
Droppable.defaultProps = {
|
||||
|
@ -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 (
|
||||
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sortable;
|
65
src/components/drag/SortableTile.js
Normal file
65
src/components/drag/SortableTile.js
Normal file
@ -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 (
|
||||
<Box style={{ position: "relative" }}>
|
||||
<Box
|
||||
ref={setDraggableNodeRef}
|
||||
style={dragStyle}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
<Box
|
||||
ref={setDroppableNodeRef}
|
||||
style={sortDropStyle}
|
||||
bg={showDropGutter && "primary"}
|
||||
/>
|
||||
<Box ref={setGroupNodeRef} style={groupDropStyle} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sortable;
|
@ -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 (
|
||||
<DndContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragMove={handleDragMove}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<SortableContext items={groups}>
|
||||
{children}
|
||||
{renderTiles(
|
||||
groups.map((group) => (
|
||||
<SortableTile
|
||||
id={group.id}
|
||||
key={group.id}
|
||||
showDropGutter={overId === group.id}
|
||||
>
|
||||
{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)}
|
||||
</SortableTile>
|
||||
))
|
||||
)}
|
||||
{createPortal(
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{dragId && (
|
||||
|
@ -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 (
|
||||
<SortableTiles
|
||||
groups={groups}
|
||||
onGroupChange={onMapsGroup}
|
||||
renderTile={groupToMapTile}
|
||||
>
|
||||
function renderTiles(tiles) {
|
||||
return (
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<FilterBar
|
||||
onFocus={() => onTileSelect()}
|
||||
@ -142,11 +137,7 @@ function MapTiles({
|
||||
columns={layout.gridTemplate}
|
||||
onClick={() => onTileSelect()}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<Sortable id={group.id} key={group.id}>
|
||||
{groupToMapTile(group)}
|
||||
</Sortable>
|
||||
))}
|
||||
{tiles}
|
||||
</Grid>
|
||||
</SimpleBar>
|
||||
{databaseDisabled && (
|
||||
@ -205,7 +196,16 @@ function MapTiles({
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</SortableTiles>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableTiles
|
||||
groups={groups}
|
||||
onGroupChange={onMapsGroup}
|
||||
renderTile={renderTile}
|
||||
renderTiles={renderTiles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
<SortableTiles
|
||||
groups={groups}
|
||||
onGroupChange={onTokensGroup}
|
||||
renderTile={groupToTokenTile}
|
||||
>
|
||||
function renderTiles(tiles) {
|
||||
return (
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<FilterBar
|
||||
onFocus={() => onTileSelect()}
|
||||
@ -136,11 +131,7 @@ function TokenTiles({
|
||||
columns={layout.gridTemplate}
|
||||
onClick={() => onTileSelect()}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<Sortable id={group.id} key={group.id}>
|
||||
{groupToTokenTile(group)}
|
||||
</Sortable>
|
||||
))}
|
||||
{tiles}
|
||||
</Grid>
|
||||
</SimpleBar>
|
||||
{databaseDisabled && (
|
||||
@ -199,7 +190,16 @@ function TokenTiles({
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</SortableTiles>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableTiles
|
||||
groups={groups}
|
||||
onGroupChange={onTokensGroup}
|
||||
renderTile={renderTile}
|
||||
renderTiles={renderTiles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user