Add token group drag from token bar

This commit is contained in:
Mitchell McCaffrey 2021-06-03 16:05:57 +10:00
parent 1ae9ce06cb
commit 597141d7fb
3 changed files with 158 additions and 71 deletions

View File

@ -2,7 +2,13 @@ import React, { useState, useRef } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Box, Flex } from "theme-ui"; import { Box, Flex } from "theme-ui";
import SimpleBar from "simplebar-react"; import SimpleBar from "simplebar-react";
import { DragOverlay, DndContext } from "@dnd-kit/core"; import {
DragOverlay,
DndContext,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import TokenBarToken from "./TokenBarToken"; import TokenBarToken from "./TokenBarToken";
import TokenBarTokenGroup from "./TokenBarTokenGroup"; import TokenBarTokenGroup from "./TokenBarTokenGroup";
@ -20,6 +26,8 @@ import {
createTokenState, createTokenState,
clientPositionToMapPosition, clientPositionToMapPosition,
} from "../../helpers/token"; } from "../../helpers/token";
import { findGroup } from "../../helpers/group";
import Vector2 from "../../helpers/Vector2";
function TokenBar({ onMapTokensStateCreate }) { function TokenBar({ onMapTokensStateCreate }) {
const { userId } = useAuth(); const { userId } = useAuth();
@ -34,6 +42,11 @@ function TokenBar({ onMapTokensStateCreate }) {
// https://github.com/clauderic/dnd-kit/issues/238 // https://github.com/clauderic/dnd-kit/issues/238
const dragOverlayRef = useRef(); const dragOverlayRef = useRef();
const pointerSensor = useSensor(PointerSensor, {
activationConstraint: { distance: 5 },
});
const sensors = useSensors(pointerSensor);
function handleDragStart({ active }) { function handleDragStart({ active }) {
setDragId(active.id); setDragId(active.id);
} }
@ -50,44 +63,67 @@ function TokenBar({ onMapTokensStateCreate }) {
y: dragRect.top + dragRect.height / 2, y: dragRect.top + dragRect.height / 2,
}; };
const mapPosition = clientPositionToMapPosition(mapStage, dragPosition); const mapPosition = clientPositionToMapPosition(mapStage, dragPosition);
const token = tokensById[active.id]; const group = findGroup(tokenGroups, active.id);
if (token && mapPosition) { if (group && mapPosition) {
const tokenState = createTokenState(token, mapPosition, userId); if (group.type === "item") {
onMapTokensStateCreate([tokenState]); const token = tokensById[group.id];
const tokenState = createTokenState(token, mapPosition, userId);
onMapTokensStateCreate([tokenState]);
} else {
let tokenStates = [];
let offset = new Vector2(0, 0);
for (let item of group.items) {
const token = tokensById[item.id];
if (token) {
tokenStates.push(
createTokenState(
token,
Vector2.add(mapPosition, offset),
userId
)
);
offset = Vector2.add(offset, 0.01);
}
}
if (tokenStates.length > 0) {
onMapTokensStateCreate(tokenStates);
}
}
} }
} }
} }
function renderTokens() { function renderToken(group, draggable = true) {
let tokens = []; if (group.type === "item") {
for (let group of tokenGroups) { const token = tokensById[group.id];
if (group.type === "item") { if (token && !token.hideInSidebar) {
const token = tokensById[group.id]; if (draggable) {
if (token && !token.hideInSidebar) { return (
tokens.push(
<Draggable id={token.id} key={token.id}> <Draggable id={token.id} key={token.id}>
<TokenBarToken token={token} /> <TokenBarToken token={token} />
</Draggable> </Draggable>
); );
} else {
return <TokenBarToken token={token} key={token.id} />;
} }
} else {
const groupTokens = [];
for (let item of group.items) {
const token = tokensById[item.id];
if (token && !token.hideInSidebar) {
groupTokens.push(token);
}
}
tokens.push(
<TokenBarTokenGroup
group={group}
tokens={groupTokens}
key={group.id}
/>
);
} }
} else {
const groupTokens = [];
for (let item of group.items) {
const token = tokensById[item.id];
if (token && !token.hideInSidebar) {
groupTokens.push(token);
}
}
return (
<TokenBarTokenGroup
group={group}
tokens={groupTokens}
key={group.id}
draggable={draggable}
/>
);
} }
return tokens;
} }
return ( return (
@ -95,6 +131,7 @@ function TokenBar({ onMapTokensStateCreate }) {
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
autoScroll={false} autoScroll={false}
sensors={sensors}
> >
<Box <Box
sx={{ sx={{
@ -113,7 +150,9 @@ function TokenBar({ onMapTokensStateCreate }) {
padding: "0 16px", padding: "0 16px",
}} }}
> >
<Flex sx={{ flexDirection: "column" }}>{renderTokens()}</Flex> <Flex sx={{ flexDirection: "column" }}>
{tokenGroups.map((group) => renderToken(group))}
</Flex>
</SimpleBar> </SimpleBar>
<Flex <Flex
bg="muted" bg="muted"
@ -136,7 +175,7 @@ function TokenBar({ onMapTokensStateCreate }) {
> >
{dragId && ( {dragId && (
<div ref={dragOverlayRef}> <div ref={dragOverlayRef}>
<TokenBarToken token={tokensById[dragId]} /> {renderToken(findGroup(tokenGroups, dragId), false)}
</div> </div>
)} )}
</DragOverlay>, </DragOverlay>,

View File

@ -21,6 +21,7 @@ function TokenBarToken({ token }) {
width: "100%", width: "100%",
height: "100%", height: "100%",
objectFit: "cover", objectFit: "cover",
pointerEvents: "none",
}} }}
alt={token.name} alt={token.name}
title={token.name} title={token.name}

View File

@ -1,84 +1,131 @@
import React, { useState } from "react"; import React, { useState, useRef } from "react";
import { Grid, Flex } from "theme-ui"; import { Grid, Flex, Box } from "theme-ui";
import { useSpring, animated } from "react-spring"; import { useSpring, animated } from "react-spring";
import { useDraggable } from "@dnd-kit/core";
import TokenImage from "./TokenImage"; import TokenImage from "./TokenImage";
import TokenBarToken from "./TokenBarToken"; import TokenBarToken from "./TokenBarToken";
import Draggable from "../drag/Draggable"; import Draggable from "../drag/Draggable";
import Vector2 from "../../helpers/Vector2";
import GroupIcon from "../../icons/GroupIcon"; import GroupIcon from "../../icons/GroupIcon";
function TokenBarTokenGroup({ group, tokens }) { function TokenBarTokenGroup({ group, tokens, draggable }) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: draggable && group.id,
disabled: !draggable,
});
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { height } = useSpring({ const { height } = useSpring({
height: isOpen ? (tokens.length + 1) * 56 : 56, height: isOpen ? (tokens.length + 1) * 56 : 56,
}); });
function renderToken(token) {
if (draggable) {
return (
<Draggable id={token.id} key={token.id}>
<TokenBarToken token={token} />
</Draggable>
);
} else {
return <TokenBarToken token={token} key={token.id} />;
}
}
function renderTokens() { function renderTokens() {
if (isOpen) { if (isOpen) {
return ( return (
<> <Grid
columns="1fr"
alt={group.name}
title={group.name}
bg="muted"
sx={{ borderRadius: "8px", gridGap: 0 }}
p={0}
>
<Flex <Flex
sx={{ sx={{
width: "48px", width: "48px",
height: "48px", height: "48px",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
cursor: "pointer", cursor: isDragging ? "grabbing" : "pointer",
color: "primary", color: "primary",
}} }}
onClick={() => setIsOpen(false)} onClick={(e) => handleOpenClick(e, false)}
key="group" key="group"
alt={group.name} alt={group.name}
title={group.name} title={group.name}
{...listeners}
{...attributes}
> >
<GroupIcon /> <GroupIcon />
</Flex> </Flex>
{tokens.map((token) => ( {tokens.map(renderToken)}
<Draggable id={token.id} key={token.id}> </Grid>
<TokenBarToken token={token} />
</Draggable>
))}
</>
); );
} else { } else {
return tokens.slice(0, 4).map((token) => ( return (
<TokenImage <Grid
token={token} columns="2fr 2fr"
key={token.id} alt={group.name}
sx={{ title={group.name}
userSelect: "none", bg="muted"
touchAction: "none", sx={{ borderRadius: "8px", gridGap: "4px" }}
pointerEvents: "none", p="2px"
}} {...listeners}
/> {...attributes}
)); >
{tokens.slice(0, 4).map((token) => (
<TokenImage
token={token}
key={token.id}
sx={{
userSelect: "none",
touchAction: "none",
pointerEvents: "none",
}}
/>
))}
</Grid>
);
}
}
// Reject the opening of a group if the pointer has moved
const clickDownPositionRef = useRef(new Vector2(0, 0));
function handleOpenDown(event) {
clickDownPositionRef.current = new Vector2(event.clientX, event.clientY);
}
function handleOpenClick(event, newOpen) {
const clickPosition = new Vector2(event.clientX, event.clientY);
const distance = Vector2.distance(
clickPosition,
clickDownPositionRef.current
);
if (distance < 5) {
setIsOpen(newOpen);
} }
} }
return ( return (
<animated.div <Box ref={setNodeRef}>
style={{ <animated.div
padding: "4px 0", style={{
width: "48px", padding: "4px 0",
height, width: "48px",
cursor: isOpen ? "default" : "pointer", height,
}} cursor: isOpen ? "default" : isDragging ? "grabbing" : "pointer",
onClick={() => !isOpen && setIsOpen(true)} }}
> onPointerDown={handleOpenDown}
<Grid onClick={(e) => !isOpen && handleOpenClick(e, true)}
columns={isOpen ? "1fr" : "2fr 2fr"}
alt={group.name}
title={group.name}
bg="muted"
sx={{ borderRadius: "8px", gridGap: isOpen ? 0 : "4px" }}
p={isOpen ? 0 : "2px"}
> >
{renderTokens()} {renderTokens()}
</Grid> </animated.div>
</animated.div> </Box>
); );
} }