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 }) {
|
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 = {
|
||||||
|
@ -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,
|
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 && (
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user