Refactored to shared sortable tiles
This commit is contained in:
parent
ac50b91d2b
commit
aece31f89a
@ -13,11 +13,10 @@ function Sortable({ id, children }) {
|
||||
|
||||
const style = {
|
||||
cursor: "pointer",
|
||||
touchAction: "none",
|
||||
opacity: isDragging ? 0.25 : undefined,
|
||||
transform:
|
||||
transform && `translate3d(${transform.x}px, ${transform.y}px, 0px)`,
|
||||
zIndex: isDragging ? 100 : 0,
|
||||
zIndex: isDragging ? 100 : undefined,
|
||||
transition,
|
||||
};
|
||||
|
67
src/components/drag/SortableTiles.js
Normal file
67
src/components/drag/SortableTiles.js
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
|
||||
import { animated, useSpring, config } from "react-spring";
|
||||
|
||||
function SortableTiles({ groups, onGroupChange, renderTile, children }) {
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
});
|
||||
const touchSensor = useSensor(TouchSensor, {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
});
|
||||
|
||||
const sensors = useSensors(mouseSensor, touchSensor);
|
||||
|
||||
const [dragId, setDragId] = useState();
|
||||
|
||||
function handleDragStart({ active }) {
|
||||
setDragId(active.id);
|
||||
}
|
||||
|
||||
function handleDragEnd({ active, over }) {
|
||||
setDragId();
|
||||
if (active && over && active.id !== over.id) {
|
||||
const oldIndex = groups.indexOf(active.id);
|
||||
const newIndex = groups.indexOf(over.id);
|
||||
onGroupChange(arrayMove(groups, oldIndex, newIndex));
|
||||
}
|
||||
}
|
||||
|
||||
const dragBounce = useSpring({
|
||||
transform: !!dragId ? "scale(1.05)" : "scale(1)",
|
||||
config: config.wobbly,
|
||||
});
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
>
|
||||
<SortableContext items={groups}>
|
||||
{children}
|
||||
{createPortal(
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{dragId && (
|
||||
<animated.div style={dragBounce}>
|
||||
{renderTile(dragId)}
|
||||
</animated.div>
|
||||
)}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
export default SortableTiles;
|
@ -1,9 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import React from "react";
|
||||
import { Flex, Box, Text, IconButton, Close, Grid } from "theme-ui";
|
||||
import SimpleBar from "simplebar-react";
|
||||
import { DndContext, DragOverlay } from "@dnd-kit/core";
|
||||
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
|
||||
|
||||
import RemoveMapIcon from "../../icons/RemoveMapIcon";
|
||||
import ResetMapIcon from "../../icons/ResetMapIcon";
|
||||
@ -11,7 +8,9 @@ import ResetMapIcon from "../../icons/ResetMapIcon";
|
||||
import MapTile from "./MapTile";
|
||||
import Link from "../Link";
|
||||
import FilterBar from "../FilterBar";
|
||||
import Sortable from "../Sortable";
|
||||
|
||||
import Sortable from "../drag/Sortable";
|
||||
import SortableTiles from "../drag/SortableTiles";
|
||||
|
||||
import { useDatabase } from "../../contexts/DatabaseContext";
|
||||
|
||||
@ -36,7 +35,6 @@ function MapTiles({
|
||||
}) {
|
||||
const { databaseStatus } = useDatabase();
|
||||
const layout = useResponsiveLayout();
|
||||
const [dragId, setDragId] = useState();
|
||||
|
||||
let hasMapState = false;
|
||||
for (let state of selectedMapStates) {
|
||||
@ -55,7 +53,8 @@ function MapTiles({
|
||||
(map) => map.type === "default"
|
||||
);
|
||||
|
||||
function mapToTile(map) {
|
||||
function mapToTile(mapId) {
|
||||
const map = maps.find((map) => map.id === mapId);
|
||||
const isSelected = selectedMaps.includes(map);
|
||||
return (
|
||||
<MapTile
|
||||
@ -76,122 +75,105 @@ function MapTiles({
|
||||
|
||||
const multipleSelected = selectedMaps.length > 1;
|
||||
|
||||
function handleDragStart({ active }) {
|
||||
setDragId(active.id);
|
||||
}
|
||||
|
||||
function handleDragEnd({ active, over }) {
|
||||
setDragId();
|
||||
if (active && over && active.id !== over.id) {
|
||||
const oldIndex = groups.indexOf(active.id);
|
||||
const newIndex = groups.indexOf(over.id);
|
||||
onMapsGroup(arrayMove(groups, oldIndex, newIndex));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={groups}>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<FilterBar
|
||||
onFocus={() => onMapSelect()}
|
||||
search={search}
|
||||
onSearchChange={onSearchChange}
|
||||
selectMode={selectMode}
|
||||
onSelectModeChange={onSelectModeChange}
|
||||
onAdd={onMapAdd}
|
||||
addTitle="Add Map"
|
||||
/>
|
||||
<SimpleBar
|
||||
style={{
|
||||
height: layout.screenSize === "large" ? "600px" : "400px",
|
||||
<SortableTiles
|
||||
groups={groups}
|
||||
onGroupChange={onMapsGroup}
|
||||
renderTile={mapToTile}
|
||||
>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<FilterBar
|
||||
onFocus={() => onMapSelect()}
|
||||
search={search}
|
||||
onSearchChange={onSearchChange}
|
||||
selectMode={selectMode}
|
||||
onSelectModeChange={onSelectModeChange}
|
||||
onAdd={onMapAdd}
|
||||
addTitle="Add Map"
|
||||
/>
|
||||
<SimpleBar
|
||||
style={{
|
||||
height: layout.screenSize === "large" ? "600px" : "400px",
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
p={2}
|
||||
pb={4}
|
||||
pt={databaseStatus === "disabled" ? 4 : 2}
|
||||
bg="muted"
|
||||
sx={{
|
||||
borderRadius: "4px",
|
||||
minHeight: layout.screenSize === "large" ? "600px" : "400px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
gap={2}
|
||||
columns={layout.gridTemplate}
|
||||
onClick={() => onMapSelect()}
|
||||
>
|
||||
<Grid
|
||||
p={2}
|
||||
pb={4}
|
||||
pt={databaseStatus === "disabled" ? 4 : 2}
|
||||
bg="muted"
|
||||
sx={{
|
||||
borderRadius: "4px",
|
||||
minHeight: layout.screenSize === "large" ? "600px" : "400px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
gap={2}
|
||||
columns={layout.gridTemplate}
|
||||
onClick={() => onMapSelect()}
|
||||
>
|
||||
{groups.map((mapId) => (
|
||||
<Sortable id={mapId} key={mapId}>
|
||||
{mapToTile(maps.find((map) => map.id === mapId))}
|
||||
</Sortable>
|
||||
))}
|
||||
</Grid>
|
||||
</SimpleBar>
|
||||
{databaseStatus === "disabled" && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "39px",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
borderRadius: "2px",
|
||||
}}
|
||||
bg="highlight"
|
||||
p={1}
|
||||
>
|
||||
<Text as="p" variant="body2">
|
||||
Map saving is unavailable. See <Link to="/faq#saving">FAQ</Link>{" "}
|
||||
for more information.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{selectedMaps.length > 0 && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
bg="overlay"
|
||||
>
|
||||
<Close
|
||||
title="Clear Selection"
|
||||
aria-label="Clear Selection"
|
||||
onClick={() => onMapSelect()}
|
||||
/>
|
||||
<Flex>
|
||||
<IconButton
|
||||
aria-label={multipleSelected ? "Reset Maps" : "Reset Map"}
|
||||
title={multipleSelected ? "Reset Maps" : "Reset Map"}
|
||||
onClick={() => onMapsReset()}
|
||||
disabled={!hasMapState}
|
||||
>
|
||||
<ResetMapIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={multipleSelected ? "Remove Maps" : "Remove Map"}
|
||||
title={multipleSelected ? "Remove Maps" : "Remove Map"}
|
||||
onClick={() => onMapsRemove()}
|
||||
disabled={hasSelectedDefaultMap}
|
||||
>
|
||||
<RemoveMapIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
{createPortal(
|
||||
<DragOverlay>
|
||||
{dragId && mapToTile(maps.find((maps) => maps.id === dragId))}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
{groups.map((mapId) => (
|
||||
<Sortable id={mapId} key={mapId}>
|
||||
{mapToTile(mapId)}
|
||||
</Sortable>
|
||||
))}
|
||||
</Grid>
|
||||
</SimpleBar>
|
||||
{databaseStatus === "disabled" && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "39px",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
borderRadius: "2px",
|
||||
}}
|
||||
bg="highlight"
|
||||
p={1}
|
||||
>
|
||||
<Text as="p" variant="body2">
|
||||
Map saving is unavailable. See <Link to="/faq#saving">FAQ</Link>{" "}
|
||||
for more information.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
{selectedMaps.length > 0 && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
bg="overlay"
|
||||
>
|
||||
<Close
|
||||
title="Clear Selection"
|
||||
aria-label="Clear Selection"
|
||||
onClick={() => onMapSelect()}
|
||||
/>
|
||||
<Flex>
|
||||
<IconButton
|
||||
aria-label={multipleSelected ? "Reset Maps" : "Reset Map"}
|
||||
title={multipleSelected ? "Reset Maps" : "Reset Map"}
|
||||
onClick={() => onMapsReset()}
|
||||
disabled={!hasMapState}
|
||||
>
|
||||
<ResetMapIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={multipleSelected ? "Remove Maps" : "Remove Map"}
|
||||
title={multipleSelected ? "Remove Maps" : "Remove Map"}
|
||||
onClick={() => onMapsRemove()}
|
||||
disabled={hasSelectedDefaultMap}
|
||||
>
|
||||
<RemoveMapIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</SortableTiles>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { DragOverlay, DndContext } from "@dnd-kit/core";
|
||||
|
||||
import ListToken from "./ListToken";
|
||||
import SelectTokensButton from "./SelectTokensButton";
|
||||
import Draggable from "../Draggable";
|
||||
import Draggable from "../drag/Draggable";
|
||||
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import React from "react";
|
||||
import { Flex, Box, Text, IconButton, Close, Grid } from "theme-ui";
|
||||
import SimpleBar from "simplebar-react";
|
||||
import { DndContext, DragOverlay } from "@dnd-kit/core";
|
||||
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
|
||||
|
||||
import RemoveTokenIcon from "../../icons/RemoveTokenIcon";
|
||||
import TokenHideIcon from "../../icons/TokenHideIcon";
|
||||
@ -12,7 +9,9 @@ import TokenShowIcon from "../../icons/TokenShowIcon";
|
||||
import TokenTile from "./TokenTile";
|
||||
import Link from "../Link";
|
||||
import FilterBar from "../FilterBar";
|
||||
import Sortable from "../Sortable";
|
||||
|
||||
import Sortable from "../drag/Sortable";
|
||||
import SortableTiles from "../drag/SortableTiles";
|
||||
|
||||
import { useDatabase } from "../../contexts/DatabaseContext";
|
||||
|
||||
@ -35,14 +34,14 @@ function TokenTiles({
|
||||
}) {
|
||||
const { databaseStatus } = useDatabase();
|
||||
const layout = useResponsiveLayout();
|
||||
const [dragId, setDragId] = useState();
|
||||
|
||||
let hasSelectedDefaultToken = selectedTokens.some(
|
||||
(token) => token.type === "default"
|
||||
);
|
||||
let allTokensVisible = selectedTokens.every((token) => !token.hideInSidebar);
|
||||
|
||||
function tokenToTile(token) {
|
||||
function tokenToTile(tokenId) {
|
||||
const token = tokens.find((token) => token.id === tokenId);
|
||||
const isSelected = selectedTokens.includes(token);
|
||||
return (
|
||||
<TokenTile
|
||||
@ -79,123 +78,104 @@ function TokenTiles({
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragStart({ active }) {
|
||||
setDragId(active.id);
|
||||
}
|
||||
|
||||
function handleDragEnd({ active, over }) {
|
||||
setDragId();
|
||||
if (active && over && active.id !== over.id) {
|
||||
const oldIndex = groups.indexOf(active.id);
|
||||
const newIndex = groups.indexOf(over.id);
|
||||
onTokensGroup(arrayMove(groups, oldIndex, newIndex));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={groups}>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<FilterBar
|
||||
onFocus={() => onTokenSelect()}
|
||||
search={search}
|
||||
onSearchChange={onSearchChange}
|
||||
selectMode={selectMode}
|
||||
onSelectModeChange={onSelectModeChange}
|
||||
onAdd={onTokenAdd}
|
||||
addTitle="Add Token"
|
||||
/>
|
||||
<SimpleBar
|
||||
style={{
|
||||
height: layout.screenSize === "large" ? "600px" : "400px",
|
||||
<SortableTiles
|
||||
groups={groups}
|
||||
onGroupChange={onTokensGroup}
|
||||
renderTile={tokenToTile}
|
||||
>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<FilterBar
|
||||
onFocus={() => onTokenSelect()}
|
||||
search={search}
|
||||
onSearchChange={onSearchChange}
|
||||
selectMode={selectMode}
|
||||
onSelectModeChange={onSelectModeChange}
|
||||
onAdd={onTokenAdd}
|
||||
addTitle="Add Token"
|
||||
/>
|
||||
<SimpleBar
|
||||
style={{
|
||||
height: layout.screenSize === "large" ? "600px" : "400px",
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
p={2}
|
||||
pb={4}
|
||||
pt={databaseStatus === "disabled" ? 4 : 2}
|
||||
bg="muted"
|
||||
sx={{
|
||||
borderRadius: "4px",
|
||||
minHeight: layout.screenSize === "large" ? "600px" : "400px",
|
||||
}}
|
||||
gap={2}
|
||||
columns={layout.gridTemplate}
|
||||
onClick={() => onTokenSelect()}
|
||||
>
|
||||
<Grid
|
||||
p={2}
|
||||
pb={4}
|
||||
pt={databaseStatus === "disabled" ? 4 : 2}
|
||||
bg="muted"
|
||||
sx={{
|
||||
borderRadius: "4px",
|
||||
minHeight: layout.screenSize === "large" ? "600px" : "400px",
|
||||
}}
|
||||
gap={2}
|
||||
columns={layout.gridTemplate}
|
||||
onClick={() => onTokenSelect()}
|
||||
>
|
||||
{groups.map((tokenId) => (
|
||||
<Sortable id={tokenId} key={tokenId}>
|
||||
{tokenToTile(tokens.find((token) => token.id === tokenId))}
|
||||
</Sortable>
|
||||
))}
|
||||
</Grid>
|
||||
</SimpleBar>
|
||||
{databaseStatus === "disabled" && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "39px",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
borderRadius: "2px",
|
||||
}}
|
||||
bg="highlight"
|
||||
p={1}
|
||||
>
|
||||
<Text as="p" variant="body2">
|
||||
Token saving is unavailable. See{" "}
|
||||
<Link to="/faq#saving">FAQ</Link> for more information.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{selectedTokens.length > 0 && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
bg="overlay"
|
||||
>
|
||||
<Close
|
||||
title="Clear Selection"
|
||||
aria-label="Clear Selection"
|
||||
onClick={() => onTokenSelect()}
|
||||
/>
|
||||
<Flex>
|
||||
<IconButton
|
||||
aria-label={hideTitle}
|
||||
title={hideTitle}
|
||||
disabled={hasSelectedDefaultToken}
|
||||
onClick={() => onTokensHide(allTokensVisible)}
|
||||
>
|
||||
{allTokensVisible ? <TokenShowIcon /> : <TokenHideIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={
|
||||
multipleSelected ? "Remove Tokens" : "Remove Token"
|
||||
}
|
||||
title={multipleSelected ? "Remove Tokens" : "Remove Token"}
|
||||
onClick={() => onTokensRemove()}
|
||||
disabled={hasSelectedDefaultToken}
|
||||
>
|
||||
<RemoveTokenIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
{createPortal(
|
||||
<DragOverlay>
|
||||
{dragId && tokenToTile(tokens.find((token) => token.id === dragId))}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
{groups.map((tokenId) => (
|
||||
<Sortable id={tokenId} key={tokenId}>
|
||||
{tokenToTile(tokenId)}
|
||||
</Sortable>
|
||||
))}
|
||||
</Grid>
|
||||
</SimpleBar>
|
||||
{databaseStatus === "disabled" && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "39px",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
borderRadius: "2px",
|
||||
}}
|
||||
bg="highlight"
|
||||
p={1}
|
||||
>
|
||||
<Text as="p" variant="body2">
|
||||
Token saving is unavailable. See <Link to="/faq#saving">FAQ</Link>{" "}
|
||||
for more information.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
{selectedTokens.length > 0 && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
bg="overlay"
|
||||
>
|
||||
<Close
|
||||
title="Clear Selection"
|
||||
aria-label="Clear Selection"
|
||||
onClick={() => onTokenSelect()}
|
||||
/>
|
||||
<Flex>
|
||||
<IconButton
|
||||
aria-label={hideTitle}
|
||||
title={hideTitle}
|
||||
disabled={hasSelectedDefaultToken}
|
||||
onClick={() => onTokensHide(allTokensVisible)}
|
||||
>
|
||||
{allTokensVisible ? <TokenShowIcon /> : <TokenHideIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={multipleSelected ? "Remove Tokens" : "Remove Token"}
|
||||
title={multipleSelected ? "Remove Tokens" : "Remove Token"}
|
||||
onClick={() => onTokensRemove()}
|
||||
disabled={hasSelectedDefaultToken}
|
||||
>
|
||||
<RemoveTokenIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</SortableTiles>
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user