Refactored to shared sortable tiles

This commit is contained in:
Mitchell McCaffrey 2021-05-13 16:26:59 +10:00
parent ac50b91d2b
commit aece31f89a
7 changed files with 270 additions and 242 deletions

View File

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

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

View File

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

View File

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

View File

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