Add ability to drag and drop a tile onto the map and added better drag cursors
This commit is contained in:
parent
f84baab6fb
commit
bac3101886
@ -13,6 +13,7 @@ function SortableTile({
|
||||
hidden,
|
||||
children,
|
||||
isDragging,
|
||||
cursor,
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
@ -29,7 +30,7 @@ function SortableTile({
|
||||
});
|
||||
|
||||
const dragStyle = {
|
||||
cursor: "pointer",
|
||||
cursor,
|
||||
opacity: isDragging ? 0.25 : undefined,
|
||||
};
|
||||
|
||||
@ -92,4 +93,8 @@ function SortableTile({
|
||||
);
|
||||
}
|
||||
|
||||
SortableTile.defaultProps = {
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
export default SortableTile;
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
function SortableTiles({ renderTile, subgroup }) {
|
||||
const { dragId, overId } = useTileDrag();
|
||||
const { dragId, overId, dragCursor } = useTileDrag();
|
||||
const {
|
||||
groups: allGroups,
|
||||
selectedGroupIds: allSelectedIds,
|
||||
@ -88,6 +88,7 @@ function SortableTiles({ renderTile, subgroup }) {
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${coords[index].x}%, ${coords[index].y}%)`,
|
||||
cursor: dragCursor,
|
||||
}}
|
||||
>
|
||||
<animated.div style={dragBounce}>
|
||||
@ -137,6 +138,7 @@ function SortableTiles({ renderTile, subgroup }) {
|
||||
disableSorting={disableSorting}
|
||||
hidden={group.id === openGroupId}
|
||||
isDragging={isDragging}
|
||||
cursor={dragCursor}
|
||||
>
|
||||
{renderSortableGroup(group, selectedGroups)}
|
||||
</SortableTile>
|
||||
|
@ -24,7 +24,6 @@ function Tile({
|
||||
borderRadius: "4px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
overflow: "hidden",
|
||||
userSelect: "none",
|
||||
}}
|
||||
|
54
src/components/tile/TilesAddDroppable.js
Normal file
54
src/components/tile/TilesAddDroppable.js
Normal file
@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import Droppable from "../drag/Droppable";
|
||||
|
||||
import { ADD_TO_MAP_ID_PREFIX } from "../../contexts/TileDragContext";
|
||||
|
||||
function TilesAddDroppable({ containerSize }) {
|
||||
return createPortal(
|
||||
<div>
|
||||
<Droppable
|
||||
id={`${ADD_TO_MAP_ID_PREFIX}-1`}
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: `calc(50vh - ${containerSize.height / 2}px)`,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${ADD_TO_MAP_ID_PREFIX}-2`}
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: `calc(50vh - ${containerSize.height / 2}px)`,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${ADD_TO_MAP_ID_PREFIX}-3`}
|
||||
style={{
|
||||
width: `calc(50vw - ${containerSize.width / 2}px)`,
|
||||
height: "100vh",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${ADD_TO_MAP_ID_PREFIX}-4`}
|
||||
style={{
|
||||
width: `calc(50vw - ${containerSize.width / 2}px)`,
|
||||
height: "100vh",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
export default TilesAddDroppable;
|
@ -1,14 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Box, Close, Grid, useThemeUI } from "theme-ui";
|
||||
import { useSpring, animated, config } from "react-spring";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
import SimpleBar from "simplebar-react";
|
||||
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
import { UNGROUP_ID_PREFIX } from "../../contexts/TileDragContext";
|
||||
|
||||
import Droppable from "../drag/Droppable";
|
||||
import TilesUngroupDroppable from "./TilesUngroupDroppable";
|
||||
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
@ -25,73 +23,47 @@ function TilesOverlay({ children }) {
|
||||
config: config.gentle,
|
||||
});
|
||||
|
||||
const [containerSize, setContinerSize] = useState(0);
|
||||
function handleResize(width, height) {
|
||||
const [containerSize, setContinerSize] = useState({ width: 0, height: 0 });
|
||||
function handleContainerResize(width, height) {
|
||||
const size = Math.min(width, height) - 16;
|
||||
setContinerSize(size);
|
||||
setContinerSize({ width: size, height: size });
|
||||
}
|
||||
|
||||
function renderUngroupBoxes() {
|
||||
return createPortal(
|
||||
<div>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-1`}
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: `calc(50vh - ${containerSize / 2}px)`,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-2`}
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: `calc(50vh - ${containerSize / 2}px)`,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-3`}
|
||||
style={{
|
||||
width: `calc(50vw - ${containerSize / 2}px)`,
|
||||
height: "100vh",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-4`}
|
||||
style={{
|
||||
width: `calc(50vw - ${containerSize / 2}px)`,
|
||||
height: "100vh",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
const [overlaySize, setOverlaySize] = useState({ width: 0, height: 0 });
|
||||
function handleOverlayResize(width, height) {
|
||||
setOverlaySize({ width, height });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{openGroupId && renderUngroupBoxes()}
|
||||
{openGroupId && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
top: 0,
|
||||
}}
|
||||
bg="overlay"
|
||||
<TilesUngroupDroppable
|
||||
innerContainerSize={containerSize}
|
||||
outerContainerSize={overlaySize}
|
||||
/>
|
||||
)}
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
|
||||
{openGroupId && (
|
||||
<ReactResizeDetector
|
||||
handleWidth
|
||||
handleHeight
|
||||
onResize={handleOverlayResize}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
top: 0,
|
||||
}}
|
||||
bg="overlay"
|
||||
/>
|
||||
</ReactResizeDetector>
|
||||
)}
|
||||
<ReactResizeDetector
|
||||
handleWidth
|
||||
handleHeight
|
||||
onResize={handleContainerResize}
|
||||
>
|
||||
<animated.div
|
||||
style={{
|
||||
...openAnimation,
|
||||
@ -109,8 +81,8 @@ function TilesOverlay({ children }) {
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: containerSize,
|
||||
height: containerSize,
|
||||
width: containerSize.width,
|
||||
height: containerSize.height,
|
||||
borderRadius: "8px",
|
||||
border: "1px solid",
|
||||
borderColor: "border",
|
||||
@ -125,8 +97,8 @@ function TilesOverlay({ children }) {
|
||||
>
|
||||
<SimpleBar
|
||||
style={{
|
||||
width: containerSize - 16,
|
||||
height: containerSize - 48,
|
||||
width: containerSize.width - 16,
|
||||
height: containerSize.height - 48,
|
||||
marginBottom: "8px",
|
||||
backgroundColor: theme.colors.muted,
|
||||
}}
|
||||
|
59
src/components/tile/TilesUngroupDroppable.js
Normal file
59
src/components/tile/TilesUngroupDroppable.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import Droppable from "../drag/Droppable";
|
||||
|
||||
import { UNGROUP_ID_PREFIX } from "../../contexts/TileDragContext";
|
||||
|
||||
function TilesUngroupDroppable({ outerContainerSize, innerContainerSize }) {
|
||||
const width = (outerContainerSize.width - innerContainerSize.width) / 2;
|
||||
const height = (outerContainerSize.height - innerContainerSize.height) / 2;
|
||||
|
||||
return createPortal(
|
||||
<div>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-1`}
|
||||
style={{
|
||||
width: outerContainerSize.width,
|
||||
height,
|
||||
position: "absolute",
|
||||
top: `calc(50% - ${innerContainerSize.height / 2 + height}px)`,
|
||||
left: `calc(50% - ${outerContainerSize.width / 2}px)`,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-2`}
|
||||
style={{
|
||||
width: outerContainerSize.width,
|
||||
height,
|
||||
position: "absolute",
|
||||
top: `calc(50% + ${innerContainerSize.height / 2}px)`,
|
||||
left: `calc(50% - ${outerContainerSize.width / 2}px)`,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-3`}
|
||||
style={{
|
||||
width,
|
||||
height: outerContainerSize.height,
|
||||
position: "absolute",
|
||||
top: `calc(50% - ${outerContainerSize.height / 2}px)`,
|
||||
left: `calc(50% - ${innerContainerSize.width / 2 + width}px)`,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-4`}
|
||||
style={{
|
||||
width,
|
||||
height: outerContainerSize.height,
|
||||
position: "absolute",
|
||||
top: `calc(50% - ${outerContainerSize.height / 2}px)`,
|
||||
left: `calc(50% + ${innerContainerSize.width / 2}px)`,
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
export default TilesUngroupDroppable;
|
@ -5,7 +5,7 @@ import SelectTokensIcon from "../../icons/SelectTokensIcon";
|
||||
|
||||
import SelectTokensModal from "../../modals/SelectTokensModal";
|
||||
|
||||
function SelectTokensButton() {
|
||||
function SelectTokensButton({ onMapTokenStateCreate }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
function openModal() {
|
||||
setIsModalOpen(true);
|
||||
@ -30,6 +30,7 @@ function SelectTokensButton() {
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={closeModal}
|
||||
onDone={handleDone}
|
||||
onMapTokenStateCreate={onMapTokenStateCreate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -16,7 +16,10 @@ import { useTokenData } from "../../contexts/TokenDataContext";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
|
||||
import { createTokenState } from "../../helpers/token";
|
||||
import {
|
||||
createTokenState,
|
||||
clientPositionToMapPosition,
|
||||
} from "../../helpers/token";
|
||||
|
||||
function TokenBar({ onMapTokenStateCreate }) {
|
||||
const { userId } = useAuth();
|
||||
@ -41,38 +44,15 @@ function TokenBar({ onMapTokenStateCreate }) {
|
||||
const mapStage = mapStageRef.current;
|
||||
const dragOverlay = dragOverlayRef.current;
|
||||
if (mapStage && dragOverlay) {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
const map = document.querySelector(".map");
|
||||
const mapRect = map.getBoundingClientRect();
|
||||
const dragRect = dragOverlay.getBoundingClientRect();
|
||||
|
||||
const dragPosition = {
|
||||
x: dragRect.left + dragRect.width / 2,
|
||||
y: dragRect.top + dragRect.height / 2,
|
||||
};
|
||||
|
||||
// Check map bounds
|
||||
if (dragPosition.x < mapRect.left || dragPosition.x > mapRect.right) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert relative to map rect
|
||||
const mapPosition = {
|
||||
x: dragPosition.x - mapRect.left,
|
||||
y: dragPosition.y - mapRect.top,
|
||||
};
|
||||
|
||||
// Convert relative to map image
|
||||
const transform = mapImage.getAbsoluteTransform().copy().invert();
|
||||
const relativePosition = transform.point(mapPosition);
|
||||
const normalizedPosition = {
|
||||
x: relativePosition.x / mapImage.width(),
|
||||
y: relativePosition.y / mapImage.height(),
|
||||
};
|
||||
|
||||
const mapPosition = clientPositionToMapPosition(mapStage, dragPosition);
|
||||
const token = tokensById[active.id];
|
||||
if (token) {
|
||||
const tokenState = createTokenState(token, normalizedPosition, userId);
|
||||
if (token && mapPosition) {
|
||||
const tokenState = createTokenState(token, mapPosition, userId);
|
||||
onMapTokenStateCreate(tokenState);
|
||||
}
|
||||
}
|
||||
@ -143,7 +123,7 @@ function TokenBar({ onMapTokenStateCreate }) {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<SelectTokensButton />
|
||||
<SelectTokensButton onMapTokenStateCreate={onMapTokenStateCreate} />
|
||||
</Flex>
|
||||
{createPortal(
|
||||
<DragOverlay
|
||||
|
@ -19,8 +19,9 @@ export const BASE_SORTABLE_ID = "__base__";
|
||||
export const GROUP_SORTABLE_ID = "__group__";
|
||||
export const GROUP_ID_PREFIX = "__group__";
|
||||
export const UNGROUP_ID_PREFIX = "__ungroup__";
|
||||
export const ADD_TO_MAP_ID_PREFIX = "__add__";
|
||||
|
||||
export function TileDragProvider({ children }) {
|
||||
export function TileDragProvider({ onDragAdd, children }) {
|
||||
const {
|
||||
groups: allGroups,
|
||||
openGroupId,
|
||||
@ -46,6 +47,7 @@ export function TileDragProvider({ children }) {
|
||||
|
||||
const [dragId, setDragId] = useState();
|
||||
const [overId, setOverId] = useState();
|
||||
const [dragCursor, setDragCursor] = useState("pointer");
|
||||
|
||||
function handleDragStart({ active, over }) {
|
||||
setDragId(active.id);
|
||||
@ -57,11 +59,21 @@ export function TileDragProvider({ children }) {
|
||||
|
||||
function handleDragOver({ over }) {
|
||||
setOverId(over?.id);
|
||||
if (over) {
|
||||
if (over.id.startsWith(UNGROUP_ID_PREFIX)) {
|
||||
setDragCursor("alias");
|
||||
} else if (over.id.startsWith(ADD_TO_MAP_ID_PREFIX)) {
|
||||
setDragCursor("copy");
|
||||
} else {
|
||||
setDragCursor("pointer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnd({ active, over }) {
|
||||
setDragId();
|
||||
setOverId();
|
||||
setDragCursor("pointer");
|
||||
if (!active || !over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
@ -94,6 +106,8 @@ export function TileDragProvider({ children }) {
|
||||
}
|
||||
onGroupsChange(newGroups);
|
||||
onGroupSelect();
|
||||
} else if (over.id.startsWith(ADD_TO_MAP_ID_PREFIX)) {
|
||||
onDragAdd && onDragAdd(selectedGroupIds, over.rect);
|
||||
} else {
|
||||
// Hanlde tile move
|
||||
const overGroupIndex = groups.findIndex((group) => group.id === over.id);
|
||||
@ -105,6 +119,7 @@ export function TileDragProvider({ children }) {
|
||||
}
|
||||
|
||||
function customCollisionDetection(rects, rect) {
|
||||
// Handle group rects
|
||||
if (groupOpen) {
|
||||
const ungroupRects = rects.filter(([id]) =>
|
||||
id.startsWith(UNGROUP_ID_PREFIX)
|
||||
@ -115,12 +130,21 @@ export function TileDragProvider({ children }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle add to map rects
|
||||
const addRects = rects.filter(([id]) =>
|
||||
id.startsWith(ADD_TO_MAP_ID_PREFIX)
|
||||
);
|
||||
const intersectingAddRect = rectIntersection(addRects, rect);
|
||||
if (intersectingAddRect) {
|
||||
return intersectingAddRect;
|
||||
}
|
||||
|
||||
const otherRects = rects.filter(([id]) => id !== UNGROUP_ID_PREFIX);
|
||||
|
||||
return closestCenter(otherRects, rect);
|
||||
}
|
||||
|
||||
const value = { dragId, overId };
|
||||
const value = { dragId, overId, dragCursor };
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
|
@ -111,3 +111,37 @@ export async function createTokenFromFile(file, userId) {
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
export function clientPositionToMapPosition(
|
||||
mapStage,
|
||||
clientPosition,
|
||||
checkMapBounds = true
|
||||
) {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
const map = document.querySelector(".map");
|
||||
const mapRect = map.getBoundingClientRect();
|
||||
|
||||
// Check map bounds
|
||||
if (
|
||||
checkMapBounds &&
|
||||
(clientPosition.x < mapRect.left || clientPosition.x > mapRect.right)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert relative to map rect
|
||||
const mapPosition = {
|
||||
x: clientPosition.x - mapRect.left,
|
||||
y: clientPosition.y - mapRect.top,
|
||||
};
|
||||
|
||||
// Convert relative to map image
|
||||
const transform = mapImage.getAbsoluteTransform().copy().invert();
|
||||
const relativePosition = transform.point(mapPosition);
|
||||
const normalizedPosition = {
|
||||
x: relativePosition.x / mapImage.width(),
|
||||
y: relativePosition.y / mapImage.height(),
|
||||
};
|
||||
|
||||
return normalizedPosition;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Button, Flex, Label, Box } from "theme-ui";
|
||||
import { useToasts } from "react-toast-notifications";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
|
||||
import EditMapModal from "./EditMapModal";
|
||||
import ConfirmModal from "./ConfirmModal";
|
||||
@ -13,6 +14,7 @@ import MapTiles from "../components/map/MapTiles";
|
||||
|
||||
import TilesOverlay from "../components/tile/TilesOverlay";
|
||||
import TilesContainer from "../components/tile/TilesContainer";
|
||||
import TilesAddDroppable from "../components/tile/TilesAddDroppable";
|
||||
|
||||
import { groupsFromIds, itemsFromGroups, findGroup } from "../helpers/group";
|
||||
import { createMapFromFile } from "../helpers/map";
|
||||
@ -230,6 +232,11 @@ function SelectMapModal({
|
||||
|
||||
const layout = useResponsiveLayout();
|
||||
|
||||
const [modalSize, setModalSize] = useState({ width: 0, height: 0 });
|
||||
function handleModalResize(width, height) {
|
||||
setModalSize({ width, height });
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@ -245,51 +252,59 @@ function SelectMapModal({
|
||||
multiple
|
||||
ref={fileInputRef}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
}}
|
||||
<ReactResizeDetector
|
||||
handleWidth
|
||||
handleHeight
|
||||
onResize={handleModalResize}
|
||||
>
|
||||
<Label pt={2} pb={1}>
|
||||
Select or import a map
|
||||
</Label>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<GroupProvider
|
||||
groups={mapGroups}
|
||||
onGroupsChange={updateMapGroups}
|
||||
onGroupsSelect={setSelectedGroupIds}
|
||||
disabled={!isOpen}
|
||||
>
|
||||
<TileDragProvider>
|
||||
<TilesContainer>
|
||||
<MapTiles
|
||||
maps={maps}
|
||||
onMapEdit={() => setIsEditModalOpen(true)}
|
||||
onMapSelect={handleMapSelect}
|
||||
/>
|
||||
</TilesContainer>
|
||||
</TileDragProvider>
|
||||
<TileDragProvider>
|
||||
<TilesOverlay>
|
||||
<MapTiles
|
||||
maps={maps}
|
||||
onMapEdit={() => setIsEditModalOpen(true)}
|
||||
onMapSelect={handleMapSelect}
|
||||
subgroup
|
||||
/>
|
||||
</TilesOverlay>
|
||||
</TileDragProvider>
|
||||
</GroupProvider>
|
||||
</Box>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={isLoading || selectedGroupIds.length > 1}
|
||||
onClick={handleSelectClick}
|
||||
mt={2}
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</Flex>
|
||||
<Label pt={2} pb={1}>
|
||||
Select or import a map
|
||||
</Label>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<GroupProvider
|
||||
groups={mapGroups}
|
||||
onGroupsChange={updateMapGroups}
|
||||
onGroupsSelect={setSelectedGroupIds}
|
||||
disabled={!isOpen}
|
||||
>
|
||||
<TileDragProvider onDragAdd={handleSelectClick}>
|
||||
<TilesAddDroppable containerSize={modalSize} />
|
||||
<TilesContainer>
|
||||
<MapTiles
|
||||
maps={maps}
|
||||
onMapEdit={() => setIsEditModalOpen(true)}
|
||||
onMapSelect={handleMapSelect}
|
||||
/>
|
||||
</TilesContainer>
|
||||
</TileDragProvider>
|
||||
<TileDragProvider onDragAdd={handleSelectClick}>
|
||||
<TilesAddDroppable containerSize={modalSize} />
|
||||
<TilesOverlay>
|
||||
<MapTiles
|
||||
maps={maps}
|
||||
onMapEdit={() => setIsEditModalOpen(true)}
|
||||
onMapSelect={handleMapSelect}
|
||||
subgroup
|
||||
/>
|
||||
</TilesOverlay>
|
||||
</TileDragProvider>
|
||||
</GroupProvider>
|
||||
</Box>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={isLoading || selectedGroupIds.length > 1}
|
||||
onClick={handleSelectClick}
|
||||
mt={2}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</Flex>
|
||||
</ReactResizeDetector>
|
||||
</ImageDrop>
|
||||
{(isLoading || mapsLoading) && <LoadingOverlay bg="overlay" />}
|
||||
<EditMapModal
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Flex, Label, Button, Box } from "theme-ui";
|
||||
import { useToasts } from "react-toast-notifications";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
|
||||
import EditTokenModal from "./EditTokenModal";
|
||||
import ConfirmModal from "./ConfirmModal";
|
||||
@ -13,9 +14,19 @@ import TokenTiles from "../components/token/TokenTiles";
|
||||
|
||||
import TilesOverlay from "../components/tile/TilesOverlay";
|
||||
import TilesContainer from "../components/tile/TilesContainer";
|
||||
import TilesAddDroppable from "../components/tile/TilesAddDroppable";
|
||||
|
||||
import { groupsFromIds, itemsFromGroups } from "../helpers/group";
|
||||
import { createTokenFromFile } from "../helpers/token";
|
||||
import {
|
||||
groupsFromIds,
|
||||
itemsFromGroups,
|
||||
getGroupItems,
|
||||
} from "../helpers/group";
|
||||
import {
|
||||
createTokenFromFile,
|
||||
createTokenState,
|
||||
clientPositionToMapPosition,
|
||||
} from "../helpers/token";
|
||||
import Vector2 from "../helpers/Vector2";
|
||||
|
||||
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
||||
|
||||
@ -25,10 +36,11 @@ import { useKeyboard } from "../contexts/KeyboardContext";
|
||||
import { useAssets } from "../contexts/AssetsContext";
|
||||
import { GroupProvider } from "../contexts/GroupContext";
|
||||
import { TileDragProvider } from "../contexts/TileDragContext";
|
||||
import { useMapStage } from "../contexts/MapStageContext";
|
||||
|
||||
import shortcuts from "../shortcuts";
|
||||
|
||||
function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
function SelectTokensModal({ isOpen, onRequestClose, onMapTokenStateCreate }) {
|
||||
const { addToast } = useToasts();
|
||||
|
||||
const { userId } = useAuth();
|
||||
@ -41,6 +53,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
tokenGroups,
|
||||
updateTokenGroups,
|
||||
updateToken,
|
||||
tokensById,
|
||||
} = useTokenData();
|
||||
const { addAssets } = useAssets();
|
||||
|
||||
@ -146,6 +159,49 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
|
||||
const mapStageRef = useMapStage();
|
||||
function handleTokensAddToMap(groupIds, rect) {
|
||||
let clientPosition = new Vector2(
|
||||
rect.width / 2 + rect.offsetLeft,
|
||||
rect.height / 2 + rect.offsetTop
|
||||
);
|
||||
const mapStage = mapStageRef.current;
|
||||
if (!mapStage) {
|
||||
return;
|
||||
}
|
||||
|
||||
let position = clientPositionToMapPosition(mapStage, clientPosition, false);
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let id of groupIds) {
|
||||
if (id in tokensById) {
|
||||
onMapTokenStateCreate(
|
||||
createTokenState(tokensById[id], position, userId)
|
||||
);
|
||||
position = Vector2.add(position, 0.01);
|
||||
} else {
|
||||
// Check if a group is selected
|
||||
const group = tokenGroups.find(
|
||||
(group) => group.id === id && group.type === "group"
|
||||
);
|
||||
if (group) {
|
||||
// Add all tokens of group
|
||||
const items = getGroupItems(group);
|
||||
for (let item of items) {
|
||||
if (item.id in tokensById) {
|
||||
onMapTokenStateCreate(
|
||||
createTokenState(tokensById[item.id], position, userId)
|
||||
);
|
||||
position = Vector2.add(position, 0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcuts
|
||||
*/
|
||||
@ -171,6 +227,11 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
|
||||
const layout = useResponsiveLayout();
|
||||
|
||||
const [modalSize, setModalSize] = useState({ width: 0, height: 0 });
|
||||
function handleModalResize(width, height) {
|
||||
setModalSize({ width, height });
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@ -186,50 +247,58 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
}}
|
||||
<ReactResizeDetector
|
||||
handleWidth
|
||||
handleHeight
|
||||
onResize={handleModalResize}
|
||||
>
|
||||
<Label pt={2} pb={1}>
|
||||
Edit or import a token
|
||||
</Label>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<GroupProvider
|
||||
groups={tokenGroups}
|
||||
onGroupsChange={updateTokenGroups}
|
||||
onGroupsSelect={setSelectedGroupIds}
|
||||
disabled={!isOpen}
|
||||
>
|
||||
<TileDragProvider>
|
||||
<TilesContainer>
|
||||
<TokenTiles
|
||||
tokens={tokens}
|
||||
onTokenEdit={() => setIsEditModalOpen(true)}
|
||||
/>
|
||||
</TilesContainer>
|
||||
</TileDragProvider>
|
||||
<TileDragProvider>
|
||||
<TilesOverlay>
|
||||
<TokenTiles
|
||||
tokens={tokens}
|
||||
onTokenEdit={() => setIsEditModalOpen(true)}
|
||||
subgroup
|
||||
/>
|
||||
</TilesOverlay>
|
||||
</TileDragProvider>
|
||||
</GroupProvider>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={isLoading}
|
||||
onClick={onRequestClose}
|
||||
mt={2}
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Flex>
|
||||
<Label pt={2} pb={1}>
|
||||
Edit or import a token
|
||||
</Label>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<GroupProvider
|
||||
groups={tokenGroups}
|
||||
onGroupsChange={updateTokenGroups}
|
||||
onGroupsSelect={setSelectedGroupIds}
|
||||
disabled={!isOpen}
|
||||
>
|
||||
<TileDragProvider onDragAdd={handleTokensAddToMap}>
|
||||
<TilesAddDroppable containerSize={modalSize} />
|
||||
<TilesContainer>
|
||||
<TokenTiles
|
||||
tokens={tokens}
|
||||
onTokenEdit={() => setIsEditModalOpen(true)}
|
||||
/>
|
||||
</TilesContainer>
|
||||
</TileDragProvider>
|
||||
<TileDragProvider onDragAdd={handleTokensAddToMap}>
|
||||
<TilesAddDroppable containerSize={modalSize} />
|
||||
<TilesOverlay>
|
||||
<TokenTiles
|
||||
tokens={tokens}
|
||||
onTokenEdit={() => setIsEditModalOpen(true)}
|
||||
subgroup
|
||||
/>
|
||||
</TilesOverlay>
|
||||
</TileDragProvider>
|
||||
</GroupProvider>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={isLoading}
|
||||
onClick={onRequestClose}
|
||||
mt={2}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Flex>
|
||||
</ReactResizeDetector>
|
||||
</ImageDrop>
|
||||
{(isLoading || tokensLoading) && <LoadingOverlay bg="overlay" />}
|
||||
<EditTokenModal
|
||||
|
Loading…
Reference in New Issue
Block a user