Add support for group visual with sortable tiles

This commit is contained in:
Mitchell McCaffrey 2021-05-21 15:12:25 +10:00
parent d34cd98fa9
commit 9f5d0b8283
7 changed files with 204 additions and 68 deletions

View File

@ -4,11 +4,7 @@ import { useDroppable } from "@dnd-kit/core";
function Droppable({ id, children, disabled }) { function Droppable({ id, children, disabled }) {
const { setNodeRef } = useDroppable({ id, disabled }); const { setNodeRef } = useDroppable({ id, disabled });
return ( return <div ref={setNodeRef}>{children}</div>;
<div style={{ width: "100%", height: "100%" }} ref={setNodeRef}>
{children}
</div>
);
} }
Droppable.defaultProps = { Droppable.defaultProps = {

View File

@ -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;

View 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;

View File

@ -7,11 +7,16 @@ import {
TouchSensor, TouchSensor,
useSensor, useSensor,
useSensors, useSensors,
closestCenter,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import { SortableContext, arrayMove } from "@dnd-kit/sortable"; import { SortableContext, arrayMove } from "@dnd-kit/sortable";
import { animated, useSpring, config } from "react-spring"; 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, { const mouseSensor = useSensor(MouseSensor, {
activationConstraint: { delay: 250, tolerance: 5 }, activationConstraint: { delay: 250, tolerance: 5 },
}); });
@ -22,14 +27,29 @@ function SortableTiles({ groups, onGroupChange, renderTile, children }) {
const sensors = useSensors(mouseSensor, touchSensor); const sensors = useSensors(mouseSensor, touchSensor);
const [dragId, setDragId] = useState(); const [dragId, setDragId] = useState();
const [overId, setOverId] = useState();
function handleDragStart({ active }) { function handleDragStart({ active, over }) {
setDragId(active.id); setDragId(active.id);
setOverId(over?.id);
}
function handleDragMove({ over }) {
setOverId(over?.id);
} }
function handleDragEnd({ active, over }) { function handleDragEnd({ active, over }) {
setDragId(); 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 oldIndex = groups.findIndex((group) => group.id === active.id);
const newIndex = groups.findIndex((group) => group.id === over.id); const newIndex = groups.findIndex((group) => group.id === over.id);
onGroupChange(arrayMove(groups, oldIndex, newIndex)); onGroupChange(arrayMove(groups, oldIndex, newIndex));
@ -37,18 +57,41 @@ function SortableTiles({ groups, onGroupChange, renderTile, children }) {
} }
const dragBounce = useSpring({ const dragBounce = useSpring({
transform: !!dragId ? "scale(1.05)" : "scale(1)", transform: !!dragId ? "scale(0.9)" : "scale(1)",
config: config.wobbly, config: config.wobbly,
}); });
const overGroupId =
overId && overId.startsWith("__group__") && overId.slice(9);
return ( return (
<DndContext <DndContext
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter}
> >
<SortableContext items={groups}> <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( {createPortal(
<DragOverlay dropAnimation={null}> <DragOverlay dropAnimation={null}>
{dragId && ( {dragId && (

View File

@ -10,7 +10,6 @@ import MapTileGroup from "./MapTileGroup";
import Link from "../Link"; import Link from "../Link";
import FilterBar from "../FilterBar"; import FilterBar from "../FilterBar";
import Sortable from "../drag/Sortable";
import SortableTiles from "../drag/SortableTiles"; import SortableTiles from "../drag/SortableTiles";
import useResponsiveLayout from "../../hooks/useResponsiveLayout"; import useResponsiveLayout from "../../hooks/useResponsiveLayout";
@ -69,7 +68,7 @@ function MapTiles({
setHasMapState(_hasMapState); setHasMapState(_hasMapState);
}, [selectedGroupIds, maps, mapStates, groups]); }, [selectedGroupIds, maps, mapStates, groups]);
function groupToMapTile(group) { function renderTile(group) {
if (group.type === "item") { if (group.type === "item") {
const map = maps.find((map) => map.id === group.id); const map = maps.find((map) => map.id === group.id);
const isSelected = selectedGroupIds.includes(group.id); const isSelected = selectedGroupIds.includes(group.id);
@ -107,12 +106,8 @@ function MapTiles({
const multipleSelected = selectedGroupIds.length > 1; const multipleSelected = selectedGroupIds.length > 1;
return ( function renderTiles(tiles) {
<SortableTiles return (
groups={groups}
onGroupChange={onMapsGroup}
renderTile={groupToMapTile}
>
<Box sx={{ position: "relative" }}> <Box sx={{ position: "relative" }}>
<FilterBar <FilterBar
onFocus={() => onTileSelect()} onFocus={() => onTileSelect()}
@ -142,11 +137,7 @@ function MapTiles({
columns={layout.gridTemplate} columns={layout.gridTemplate}
onClick={() => onTileSelect()} onClick={() => onTileSelect()}
> >
{groups.map((group) => ( {tiles}
<Sortable id={group.id} key={group.id}>
{groupToMapTile(group)}
</Sortable>
))}
</Grid> </Grid>
</SimpleBar> </SimpleBar>
{databaseDisabled && ( {databaseDisabled && (
@ -205,7 +196,16 @@ function MapTiles({
</Flex> </Flex>
)} )}
</Box> </Box>
</SortableTiles> );
}
return (
<SortableTiles
groups={groups}
onGroupChange={onMapsGroup}
renderTile={renderTile}
renderTiles={renderTiles}
/>
); );
} }

View File

@ -11,7 +11,6 @@ import TokenTileGroup from "./TokenTileGroup";
import Link from "../Link"; import Link from "../Link";
import FilterBar from "../FilterBar"; import FilterBar from "../FilterBar";
import Sortable from "../drag/Sortable";
import SortableTiles from "../drag/SortableTiles"; import SortableTiles from "../drag/SortableTiles";
import useResponsiveLayout from "../../hooks/useResponsiveLayout"; import useResponsiveLayout from "../../hooks/useResponsiveLayout";
@ -49,7 +48,7 @@ function TokenTiles({
setAllTokensVisisble(selectedTokens.every((token) => !token.hideInSidebar)); setAllTokensVisisble(selectedTokens.every((token) => !token.hideInSidebar));
}, [selectedGroupIds, tokens, groups]); }, [selectedGroupIds, tokens, groups]);
function groupToTokenTile(group) { function renderTile(group) {
if (group.type === "item") { if (group.type === "item") {
const token = tokens.find((token) => token.id === group.id); const token = tokens.find((token) => token.id === group.id);
const isSelected = selectedGroupIds.includes(group.id); const isSelected = selectedGroupIds.includes(group.id);
@ -102,12 +101,8 @@ function TokenTiles({
} }
} }
return ( function renderTiles(tiles) {
<SortableTiles return (
groups={groups}
onGroupChange={onTokensGroup}
renderTile={groupToTokenTile}
>
<Box sx={{ position: "relative" }}> <Box sx={{ position: "relative" }}>
<FilterBar <FilterBar
onFocus={() => onTileSelect()} onFocus={() => onTileSelect()}
@ -136,11 +131,7 @@ function TokenTiles({
columns={layout.gridTemplate} columns={layout.gridTemplate}
onClick={() => onTileSelect()} onClick={() => onTileSelect()}
> >
{groups.map((group) => ( {tiles}
<Sortable id={group.id} key={group.id}>
{groupToTokenTile(group)}
</Sortable>
))}
</Grid> </Grid>
</SimpleBar> </SimpleBar>
{databaseDisabled && ( {databaseDisabled && (
@ -199,7 +190,16 @@ function TokenTiles({
</Flex> </Flex>
)} )}
</Box> </Box>
</SortableTiles> );
}
return (
<SortableTiles
groups={groups}
onGroupChange={onTokensGroup}
renderTile={renderTile}
renderTiles={renderTiles}
/>
); );
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { v4 as uuid } from "uuid";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { groupBy, keyBy } from "./shared"; 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) { export function groupsFromIds(groupIds, groups) {
const groupsByIds = keyBy(groups, "id"); const groupsByIds = keyBy(groups, "id");
const filteredGroups = []; const filteredGroups = [];
@ -144,6 +169,11 @@ export function groupsFromIds(groupIds, groups) {
return filteredGroups; return filteredGroups;
} }
/**
* Get all items from a group including all sub groups
* @param {Group} group
* @return {GroupItem[]}
*/
function getGroupItems(group) { function getGroupItems(group) {
if (group.type === "group") { if (group.type === "group") {
let groups = []; 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") { export function itemsFromGroups(groups, allItems, itemKey = "id") {
const allItemsById = keyBy(allItems, itemKey); const allItemsById = keyBy(allItems, itemKey);
const groupedItems = []; const groupedItems = [];
@ -168,3 +205,28 @@ export function itemsFromGroups(groups, allItems, itemKey = "id") {
return groupedItems; 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,
};
}
}