Move drag and drop to dnd-kit
This commit is contained in:
parent
bd522017a7
commit
8aa3bda90d
@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "^4.2.0",
|
||||
"@babylonjs/loaders": "^4.2.0",
|
||||
"@dnd-kit/core": "3.0.0",
|
||||
"@mitchemmc/dexie-export-import": "^1.0.1",
|
||||
"@msgpack/msgpack": "^2.4.1",
|
||||
"@sentry/react": "^6.2.2",
|
||||
@ -25,7 +26,6 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"fuse.js": "^6.4.6",
|
||||
"image-outline": "^0.1.0",
|
||||
"interactjs": "^1.10.8",
|
||||
"konva": "^7.2.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
|
16
src/App.js
16
src/App.js
@ -13,12 +13,8 @@ import Donate from "./routes/Donate";
|
||||
|
||||
import { AuthProvider } from "./contexts/AuthContext";
|
||||
import { DatabaseProvider } from "./contexts/DatabaseContext";
|
||||
import { MapDataProvider } from "./contexts/MapDataContext";
|
||||
import { TokenDataProvider } from "./contexts/TokenDataContext";
|
||||
import { MapLoadingProvider } from "./contexts/MapLoadingContext";
|
||||
import { SettingsProvider } from "./contexts/SettingsContext";
|
||||
import { KeyboardProvider } from "./contexts/KeyboardContext";
|
||||
import { AssetsProvider, AssetURLsProvider } from "./contexts/AssetsContext";
|
||||
|
||||
import { ToastProvider } from "./components/Toast";
|
||||
|
||||
@ -49,17 +45,7 @@ function App() {
|
||||
<FAQ />
|
||||
</Route>
|
||||
<Route path="/game/:id">
|
||||
<AssetsProvider>
|
||||
<AssetURLsProvider>
|
||||
<MapLoadingProvider>
|
||||
<MapDataProvider>
|
||||
<TokenDataProvider>
|
||||
<Game />
|
||||
</TokenDataProvider>
|
||||
</MapDataProvider>
|
||||
</MapLoadingProvider>
|
||||
</AssetURLsProvider>
|
||||
</AssetsProvider>
|
||||
<Game />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
|
27
src/components/Draggable.js
Normal file
27
src/components/Draggable.js
Normal file
@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
|
||||
function Draggable({ id, children, data }) {
|
||||
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
||||
id,
|
||||
data,
|
||||
});
|
||||
|
||||
const style = {
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
cursor: "pointer",
|
||||
touchAction: "none",
|
||||
opacity: isDragging ? 0.5 : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default Draggable;
|
18
src/components/Droppable.js
Normal file
18
src/components/Droppable.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
|
||||
function Droppable({ id, children, disabled }) {
|
||||
const { setNodeRef } = useDroppable({ id, disabled });
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%" }} ref={setNodeRef}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Droppable.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default Droppable;
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Box, IconButton } from "theme-ui";
|
||||
|
||||
import RemoveTokenIcon from "../icons/RemoveTokenIcon";
|
||||
import RemoveTokenIcon from "../../icons/RemoveTokenIcon";
|
||||
|
||||
function DragOverlay({ dragging, node, onRemove }) {
|
||||
const [isRemoveHovered, setIsRemoveHovered] = useState(false);
|
@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box } from "theme-ui";
|
||||
|
||||
import MapControls from "./MapControls";
|
||||
import MapInteraction from "./MapInteraction";
|
||||
@ -18,6 +19,8 @@ import TokenDragOverlay from "../token/TokenDragOverlay";
|
||||
import NoteMenu from "../note/NoteMenu";
|
||||
import NoteDragOverlay from "../note/NoteDragOverlay";
|
||||
|
||||
import Droppable from "../Droppable";
|
||||
|
||||
import {
|
||||
AddShapeAction,
|
||||
CutShapeAction,
|
||||
@ -336,30 +339,34 @@ function Map({
|
||||
);
|
||||
|
||||
return (
|
||||
<MapInteraction
|
||||
map={map}
|
||||
mapState={mapState}
|
||||
controls={
|
||||
<>
|
||||
{mapControls}
|
||||
{tokenMenu}
|
||||
{noteMenu}
|
||||
{tokenDragOverlay}
|
||||
{noteDragOverlay}
|
||||
</>
|
||||
}
|
||||
selectedToolId={selectedToolId}
|
||||
onSelectedToolChange={setSelectedToolId}
|
||||
disabledControls={disabledControls}
|
||||
>
|
||||
{mapGrid}
|
||||
{mapDrawing}
|
||||
{mapNotes}
|
||||
{mapTokens}
|
||||
{mapFog}
|
||||
{mapPointer}
|
||||
{mapMeasure}
|
||||
</MapInteraction>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Droppable id="map" disabled={!map}>
|
||||
<MapInteraction
|
||||
map={map}
|
||||
mapState={mapState}
|
||||
controls={
|
||||
<>
|
||||
{mapControls}
|
||||
{tokenMenu}
|
||||
{noteMenu}
|
||||
{tokenDragOverlay}
|
||||
{noteDragOverlay}
|
||||
</>
|
||||
}
|
||||
selectedToolId={selectedToolId}
|
||||
onSelectedToolChange={setSelectedToolId}
|
||||
disabledControls={disabledControls}
|
||||
>
|
||||
{mapGrid}
|
||||
{mapDrawing}
|
||||
{mapNotes}
|
||||
{mapTokens}
|
||||
{mapFog}
|
||||
{mapPointer}
|
||||
{mapMeasure}
|
||||
</MapInteraction>
|
||||
</Droppable>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -181,11 +181,12 @@ function MapInteraction({
|
||||
<GridProvider grid={map?.grid} width={mapWidth} height={mapHeight}>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
position: "relative",
|
||||
cursor: getCursorForTool(selectedToolId),
|
||||
touchAction: "none",
|
||||
outline: "none",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
ref={containerRef}
|
||||
className="map"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import DragOverlay from "../DragOverlay";
|
||||
import DragOverlay from "../map/DragOverlay";
|
||||
|
||||
function NoteDragOverlay({ onNoteRemove, noteId, noteGroup, dragging }) {
|
||||
function handleNoteRemove() {
|
||||
|
@ -7,7 +7,7 @@ import { useDataURL } from "../../contexts/AssetsContext";
|
||||
|
||||
import { tokenSources, unknownSource } from "../../tokens";
|
||||
|
||||
function ListToken({ token, className }) {
|
||||
function ListToken({ token }) {
|
||||
const tokenURL = useDataURL(
|
||||
token,
|
||||
tokenSources,
|
||||
@ -20,11 +20,10 @@ function ListToken({ token, className }) {
|
||||
usePreventTouch(imageRef);
|
||||
|
||||
return (
|
||||
<Box my={2} mx={3} sx={{ width: "48px", height: "48px" }}>
|
||||
<Box py={1} sx={{ width: "48px", height: "56px" }}>
|
||||
<Image
|
||||
src={tokenURL}
|
||||
ref={imageRef}
|
||||
className={className}
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
@ -32,8 +31,6 @@ function ListToken({ token, className }) {
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
// pass id into the dom element which is then used by the ProxyToken
|
||||
data-id={token.id}
|
||||
alt={token.name}
|
||||
title={token.name}
|
||||
/>
|
||||
|
@ -1,172 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Image, Box } from "theme-ui";
|
||||
import interact from "interactjs";
|
||||
|
||||
import usePortal from "../../hooks/usePortal";
|
||||
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
|
||||
/**
|
||||
* @callback onProxyDragEnd
|
||||
* @param {boolean} isOnMap whether the token was dropped on the map
|
||||
* @param {Object} token the token that was dropped
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tokenClassName The class name to attach the interactjs handler to
|
||||
* @param {onProxyDragEnd} onProxyDragEnd Called when the proxy token is dropped
|
||||
* @param {Object} tokens An optional mapping of tokens to use as a base when calling OnProxyDragEnd
|
||||
|
||||
*/
|
||||
function ProxyToken({ tokenClassName, onProxyDragEnd, tokens }) {
|
||||
const proxyContainer = usePortal("root");
|
||||
|
||||
const [imageSource, setImageSource] = useState("");
|
||||
const proxyRef = useRef();
|
||||
|
||||
// Store the tokens in a ref and access in the interactjs loop
|
||||
// This is needed to stop interactjs from creating multiple listeners
|
||||
const tokensRef = useRef(tokens);
|
||||
useEffect(() => {
|
||||
tokensRef.current = tokens;
|
||||
}, [tokens]);
|
||||
|
||||
const proxyOnMap = useRef(false);
|
||||
const mapStageRef = useMapStage();
|
||||
|
||||
useEffect(() => {
|
||||
interact(`.${tokenClassName}`).draggable({
|
||||
listeners: {
|
||||
start: (event) => {
|
||||
let target = event.target;
|
||||
|
||||
// Hide the token and copy it's image to the proxy
|
||||
target.parentElement.style.opacity = "0.25";
|
||||
setImageSource(target.src);
|
||||
|
||||
let proxy = proxyRef.current;
|
||||
if (proxy) {
|
||||
// Find and set the initial offset of the token to the proxy
|
||||
const proxyRect = proxy.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const xOffset = targetRect.left - proxyRect.left;
|
||||
const yOffset = targetRect.top - proxyRect.top;
|
||||
proxy.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
|
||||
proxy.setAttribute("data-x", xOffset);
|
||||
proxy.setAttribute("data-y", yOffset);
|
||||
|
||||
// Copy width and height of target
|
||||
proxy.style.width = `${targetRect.width}px`;
|
||||
proxy.style.height = `${targetRect.height}px`;
|
||||
}
|
||||
},
|
||||
|
||||
move: (event) => {
|
||||
let proxy = proxyRef.current;
|
||||
// Move the proxy based off of the movment of the token
|
||||
if (proxy) {
|
||||
// keep the dragged position in the data-x/data-y attributes
|
||||
const x =
|
||||
(parseFloat(proxy.getAttribute("data-x")) || 0) + event.dx;
|
||||
const y =
|
||||
(parseFloat(proxy.getAttribute("data-y")) || 0) + event.dy;
|
||||
proxy.style.transform = `translate(${x}px, ${y}px)`;
|
||||
|
||||
// Check whether the proxy is on the right or left hand side of the screen
|
||||
// if not set proxyOnMap to true
|
||||
const proxyRect = proxy.getBoundingClientRect();
|
||||
const map = document.querySelector(".map");
|
||||
const mapRect = map.getBoundingClientRect();
|
||||
proxyOnMap.current =
|
||||
proxyRect.left > mapRect.left && proxyRect.right < mapRect.right;
|
||||
|
||||
// update the posiion attributes
|
||||
proxy.setAttribute("data-x", x);
|
||||
proxy.setAttribute("data-y", y);
|
||||
}
|
||||
},
|
||||
|
||||
end: (event) => {
|
||||
let target = event.target;
|
||||
const id = target.dataset.id;
|
||||
let proxy = proxyRef.current;
|
||||
if (proxy) {
|
||||
const mapStage = mapStageRef.current;
|
||||
if (onProxyDragEnd && mapStage) {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
const map = document.querySelector(".map");
|
||||
const mapRect = map.getBoundingClientRect();
|
||||
const position = {
|
||||
x: event.clientX - mapRect.left,
|
||||
y: event.clientY - mapRect.top,
|
||||
};
|
||||
const transform = mapImage.getAbsoluteTransform().copy().invert();
|
||||
const relativePosition = transform.point(position);
|
||||
const normalizedPosition = {
|
||||
x: relativePosition.x / mapImage.width(),
|
||||
y: relativePosition.y / mapImage.height(),
|
||||
};
|
||||
// Get the token from the supplied tokens if it exists
|
||||
const token = tokensRef.current[id] || {};
|
||||
onProxyDragEnd(proxyOnMap.current, {
|
||||
...token,
|
||||
x: normalizedPosition.x,
|
||||
y: normalizedPosition.y,
|
||||
});
|
||||
}
|
||||
|
||||
// Reset the proxy position
|
||||
proxy.style.transform = "translate(0px, 0px)";
|
||||
proxy.setAttribute("data-x", 0);
|
||||
proxy.setAttribute("data-y", 0);
|
||||
}
|
||||
|
||||
// Show the token
|
||||
target.parentElement.style.opacity = "1";
|
||||
setImageSource("");
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [onProxyDragEnd, tokenClassName, proxyContainer, mapStageRef]);
|
||||
|
||||
if (!imageSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a portal to allow the proxy to move past the bounds of the token
|
||||
return ReactDOM.createPortal(
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ position: "absolute", display: "flex", flexDirection: "column" }}
|
||||
ref={proxyRef}
|
||||
>
|
||||
<Image
|
||||
src={imageSource}
|
||||
sx={{
|
||||
touchAction: "none",
|
||||
userSelect: "none",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>,
|
||||
proxyContainer
|
||||
);
|
||||
}
|
||||
|
||||
ProxyToken.defaultProps = {
|
||||
tokens: {},
|
||||
};
|
||||
|
||||
export default ProxyToken;
|
@ -1,98 +1,77 @@
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Box, Flex } from "theme-ui";
|
||||
import shortid from "shortid";
|
||||
import SimpleBar from "simplebar-react";
|
||||
import { DragOverlay } from "@dnd-kit/core";
|
||||
|
||||
import ListToken from "./ListToken";
|
||||
import ProxyToken from "./ProxyToken";
|
||||
|
||||
import SelectTokensButton from "./SelectTokensButton";
|
||||
|
||||
import { fromEntries } from "../../helpers/shared";
|
||||
import Draggable from "../Draggable";
|
||||
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { useTokenData } from "../../contexts/TokenDataContext";
|
||||
import { useDragId } from "../../contexts/DragContext";
|
||||
|
||||
const listTokenClassName = "list-token";
|
||||
|
||||
function TokenBar({ onMapTokenStateCreate }) {
|
||||
const { userId } = useAuth();
|
||||
const { ownedTokens, tokens } = useTokenData();
|
||||
function TokenBar() {
|
||||
const { ownedTokens } = useTokenData();
|
||||
const [fullScreen] = useSetting("map.fullScreen");
|
||||
|
||||
function handleProxyDragEnd(isOnMap, token) {
|
||||
if (isOnMap && onMapTokenStateCreate) {
|
||||
// Create a token state from the dragged token
|
||||
let tokenState = {
|
||||
id: shortid.generate(),
|
||||
tokenId: token.id,
|
||||
owner: userId,
|
||||
size: token.defaultSize,
|
||||
category: token.defaultCategory,
|
||||
label: token.defaultLabel,
|
||||
statuses: [],
|
||||
x: token.x,
|
||||
y: token.y,
|
||||
lastModifiedBy: userId,
|
||||
lastModified: Date.now(),
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
type: token.type,
|
||||
outline: token.outline,
|
||||
width: token.width,
|
||||
height: token.height,
|
||||
};
|
||||
if (token.type === "file") {
|
||||
tokenState.file = token.file;
|
||||
} else if (token.type === "default") {
|
||||
tokenState.key = token.key;
|
||||
}
|
||||
onMapTokenStateCreate(tokenState);
|
||||
}
|
||||
}
|
||||
const activeDragId = useDragId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "80px",
|
||||
minWidth: "80px",
|
||||
overflow: "hidden",
|
||||
display: fullScreen ? "none" : "block",
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "80px",
|
||||
minWidth: "80px",
|
||||
overflowY: "scroll",
|
||||
overflowX: "hidden",
|
||||
display: fullScreen ? "none" : "block",
|
||||
}}
|
||||
>
|
||||
<SimpleBar
|
||||
style={{
|
||||
height: "calc(100% - 48px)",
|
||||
overflowX: "hidden",
|
||||
padding: "0 16px",
|
||||
}}
|
||||
>
|
||||
<SimpleBar style={{ height: "calc(100% - 48px)", overflowX: "hidden" }}>
|
||||
{ownedTokens
|
||||
.filter((token) => !token.hideInSidebar)
|
||||
.map((token) => (
|
||||
<ListToken
|
||||
key={token.id}
|
||||
token={token}
|
||||
className={listTokenClassName}
|
||||
/>
|
||||
))}
|
||||
</SimpleBar>
|
||||
<Flex
|
||||
bg="muted"
|
||||
sx={{
|
||||
justifyContent: "center",
|
||||
height: "48px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<SelectTokensButton />
|
||||
</Flex>
|
||||
</Box>
|
||||
<ProxyToken
|
||||
tokenClassName={listTokenClassName}
|
||||
onProxyDragEnd={handleProxyDragEnd}
|
||||
tokens={fromEntries(tokens.map((token) => [token.id, token]))}
|
||||
/>
|
||||
</>
|
||||
{ownedTokens
|
||||
.filter((token) => !token.hideInSidebar)
|
||||
.map((token) => (
|
||||
<Draggable
|
||||
id={`sidebar-${token.id}`}
|
||||
key={token.id}
|
||||
data={{ tokenId: token.id }}
|
||||
>
|
||||
<ListToken token={token} />
|
||||
</Draggable>
|
||||
))}
|
||||
</SimpleBar>
|
||||
<Flex
|
||||
bg="muted"
|
||||
sx={{
|
||||
justifyContent: "center",
|
||||
height: "48px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<SelectTokensButton />
|
||||
</Flex>
|
||||
{createPortal(
|
||||
<DragOverlay>
|
||||
{activeDragId && (
|
||||
<ListToken
|
||||
token={ownedTokens.find(
|
||||
(token) => `sidebar-${token.id}` === activeDragId
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
useMapHeight,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
|
||||
import DragOverlay from "../DragOverlay";
|
||||
import DragOverlay from "../map/DragOverlay";
|
||||
|
||||
function TokenDragOverlay({
|
||||
onTokenStateRemove,
|
||||
|
36
src/contexts/DragContext.js
Normal file
36
src/contexts/DragContext.js
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
|
||||
/**
|
||||
* @type {React.Context<string|undefined>}
|
||||
*/
|
||||
const DragIdContext = React.createContext();
|
||||
|
||||
export function DragProvider({ children, onDragEnd }) {
|
||||
const [activeDragId, setActiveDragId] = useState(null);
|
||||
|
||||
function handleDragStart({ active }) {
|
||||
setActiveDragId(active.id);
|
||||
}
|
||||
|
||||
function handleDragEnd(event) {
|
||||
setActiveDragId(null);
|
||||
onDragEnd && onDragEnd(event);
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<DragIdContext.Provider value={activeDragId}>
|
||||
{children}
|
||||
</DragIdContext.Provider>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDragId() {
|
||||
const context = useContext(DragIdContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useDragId must be used within a DragProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useToasts } from "react-toast-notifications";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { useMapData } from "../contexts/MapDataContext";
|
||||
import { useMapLoading } from "../contexts/MapLoadingContext";
|
||||
@ -7,6 +8,9 @@ import { useAuth } from "../contexts/AuthContext";
|
||||
import { useDatabase } from "../contexts/DatabaseContext";
|
||||
import { useParty } from "../contexts/PartyContext";
|
||||
import { useAssets } from "../contexts/AssetsContext";
|
||||
import { DragProvider } from "../contexts/DragContext";
|
||||
import { useTokenData } from "../contexts/TokenDataContext";
|
||||
import { useMapStage } from "../contexts/MapStageContext";
|
||||
|
||||
import { omit } from "../helpers/shared";
|
||||
|
||||
@ -40,8 +44,10 @@ function NetworkedMapAndTokens({ session }) {
|
||||
const { userId } = useAuth();
|
||||
const partyState = useParty();
|
||||
const { assetLoadStart, assetProgressUpdate, isLoading } = useMapLoading();
|
||||
const mapStageRef = useMapStage();
|
||||
|
||||
const { updateMapState } = useMapData();
|
||||
const { tokensById } = useTokenData();
|
||||
const { getAsset, putAsset } = useAssets();
|
||||
|
||||
const [currentMap, setCurrentMap] = useState(null);
|
||||
@ -379,6 +385,53 @@ function NetworkedMapAndTokens({ session }) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragEnd({ active, over }) {
|
||||
const tokenId = active?.data?.current?.tokenId;
|
||||
const token = tokensById[tokenId];
|
||||
const mapStage = mapStageRef.current;
|
||||
if (over?.id === "map" && tokenId && token && mapStage) {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
// TODO: Get proper pointer position when dnd-kit is updated
|
||||
// https://github.com/clauderic/dnd-kit/issues/238
|
||||
const pointerPosition = {
|
||||
x: over.rect.width / 2,
|
||||
y: over.rect.height / 2,
|
||||
};
|
||||
const transform = mapImage.getAbsoluteTransform().copy().invert();
|
||||
const relativePosition = transform.point(pointerPosition);
|
||||
const normalizedPosition = {
|
||||
x: relativePosition.x / mapImage.width(),
|
||||
y: relativePosition.y / mapImage.height(),
|
||||
};
|
||||
let tokenState = {
|
||||
id: uuid(),
|
||||
tokenId: token.id,
|
||||
owner: userId,
|
||||
size: token.defaultSize,
|
||||
category: token.defaultCategory,
|
||||
label: token.defaultLabel,
|
||||
statuses: [],
|
||||
x: normalizedPosition.x,
|
||||
y: normalizedPosition.y,
|
||||
lastModifiedBy: userId,
|
||||
lastModified: Date.now(),
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
type: token.type,
|
||||
outline: token.outline,
|
||||
width: token.width,
|
||||
height: token.height,
|
||||
};
|
||||
if (token.type === "file") {
|
||||
tokenState.file = token.file;
|
||||
} else if (token.type === "default") {
|
||||
tokenState.key = token.key;
|
||||
}
|
||||
handleMapTokenStateCreate(tokenState);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function handlePeerData({ id, data, reply }) {
|
||||
if (id === "assetRequest") {
|
||||
@ -451,7 +504,7 @@ function NetworkedMapAndTokens({ session }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DragProvider onDragEnd={handleDragEnd}>
|
||||
<Map
|
||||
map={currentMap}
|
||||
mapState={currentMapState}
|
||||
@ -475,8 +528,8 @@ function NetworkedMapAndTokens({ session }) {
|
||||
disabledTokens={disabledMapTokens}
|
||||
session={session}
|
||||
/>
|
||||
<TokenBar onMapTokenStateCreate={handleMapTokenStateCreate} />
|
||||
</>
|
||||
<TokenBar />
|
||||
</DragProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,10 @@ import { MapStageProvider } from "../contexts/MapStageContext";
|
||||
import { useDatabase } from "../contexts/DatabaseContext";
|
||||
import { PlayerProvider } from "../contexts/PlayerContext";
|
||||
import { PartyProvider } from "../contexts/PartyContext";
|
||||
import { AssetsProvider, AssetURLsProvider } from "../contexts/AssetsContext";
|
||||
import { MapDataProvider } from "../contexts/MapDataContext";
|
||||
import { TokenDataProvider } from "../contexts/TokenDataContext";
|
||||
import { MapLoadingProvider } from "../contexts/MapLoadingContext";
|
||||
|
||||
import NetworkedMapAndTokens from "../network/NetworkedMapAndTokens";
|
||||
import NetworkedParty from "../network/NetworkedParty";
|
||||
@ -84,7 +88,6 @@ function Game() {
|
||||
};
|
||||
}, [session]);
|
||||
|
||||
|
||||
// Join game
|
||||
useEffect(() => {
|
||||
if (sessionStatus === "ready" && databaseStatus !== "loading") {
|
||||
@ -103,50 +106,62 @@ function Game() {
|
||||
const mapStageRef = useRef();
|
||||
|
||||
return (
|
||||
<PlayerProvider session={session}>
|
||||
<PartyProvider session={session}>
|
||||
<MapStageProvider value={mapStageRef}>
|
||||
<Flex sx={{ flexDirection: "column", height: "100%" }}>
|
||||
<Flex
|
||||
sx={{
|
||||
justifyContent: "space-between",
|
||||
flexGrow: 1,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<NetworkedParty session={session} gameId={gameId} />
|
||||
<NetworkedMapAndTokens session={session} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Banner
|
||||
isOpen={!!peerError}
|
||||
onRequestClose={() => setPeerError(null)}
|
||||
>
|
||||
<Box p={1}>
|
||||
<Text as="p" variant="body2">
|
||||
{peerError} See <Link to="/faq#connection">FAQ</Link> for more
|
||||
information.
|
||||
</Text>
|
||||
</Box>
|
||||
</Banner>
|
||||
<OfflineBanner isOpen={sessionStatus === "offline"} />
|
||||
<ReconnectBanner isOpen={sessionStatus === "reconnecting"} />
|
||||
<AuthModal
|
||||
isOpen={sessionStatus === "auth"}
|
||||
onSubmit={handleAuthSubmit}
|
||||
/>
|
||||
<GameExpiredModal
|
||||
isOpen={gameExpired}
|
||||
onRequestClose={() => setGameExpired(false)}
|
||||
/>
|
||||
<ForceUpdateModal
|
||||
isOpen={sessionStatus === "needs_update"}
|
||||
/>
|
||||
{!sessionStatus && <LoadingOverlay />}
|
||||
<MapLoadingOverlay />
|
||||
</MapStageProvider>
|
||||
</PartyProvider>
|
||||
</PlayerProvider>
|
||||
<AssetsProvider>
|
||||
<AssetURLsProvider>
|
||||
<MapLoadingProvider>
|
||||
<MapDataProvider>
|
||||
<TokenDataProvider>
|
||||
<PlayerProvider session={session}>
|
||||
<PartyProvider session={session}>
|
||||
<MapStageProvider value={mapStageRef}>
|
||||
<Flex sx={{ flexDirection: "column", height: "100%" }}>
|
||||
<Flex
|
||||
sx={{
|
||||
justifyContent: "space-between",
|
||||
flexGrow: 1,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<NetworkedParty session={session} gameId={gameId} />
|
||||
<NetworkedMapAndTokens session={session} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Banner
|
||||
isOpen={!!peerError}
|
||||
onRequestClose={() => setPeerError(null)}
|
||||
>
|
||||
<Box p={1}>
|
||||
<Text as="p" variant="body2">
|
||||
{peerError} See <Link to="/faq#connection">FAQ</Link>{" "}
|
||||
for more information.
|
||||
</Text>
|
||||
</Box>
|
||||
</Banner>
|
||||
<OfflineBanner isOpen={sessionStatus === "offline"} />
|
||||
<ReconnectBanner
|
||||
isOpen={sessionStatus === "reconnecting"}
|
||||
/>
|
||||
<AuthModal
|
||||
isOpen={sessionStatus === "auth"}
|
||||
onSubmit={handleAuthSubmit}
|
||||
/>
|
||||
<GameExpiredModal
|
||||
isOpen={gameExpired}
|
||||
onRequestClose={() => setGameExpired(false)}
|
||||
/>
|
||||
<ForceUpdateModal
|
||||
isOpen={sessionStatus === "needs_update"}
|
||||
/>
|
||||
{!sessionStatus && <LoadingOverlay />}
|
||||
<MapLoadingOverlay />
|
||||
</MapStageProvider>
|
||||
</PartyProvider>
|
||||
</PlayerProvider>
|
||||
</TokenDataProvider>
|
||||
</MapDataProvider>
|
||||
</MapLoadingProvider>
|
||||
</AssetURLsProvider>
|
||||
</AssetsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
40
yarn.lock
40
yarn.lock
@ -1796,6 +1796,29 @@
|
||||
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
|
||||
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
|
||||
|
||||
"@dnd-kit/accessibility@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz#b56e3750414fd907b7d6972b3116aa8f96d07fde"
|
||||
integrity sha512-QwaQ1IJHQHMMuAGOOYHQSx7h7vMZPfO97aDts8t5N/MY7n2QTDSnW+kF7uRQ1tVBkr6vJ+BqHWG5dlgGvwVjow==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/core@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-3.0.0.tgz#96dadb6b2dba05ab177e0190b33ae219017bc167"
|
||||
integrity sha512-QxHLfZHOLkQWK0FPbr5hefWZzsdZfDuluKPwIK1bT2lwp/4hmFXRA6ivqX3FT4g8T0d2de2C1jxYhKM4H3uMQw==
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility" "^3.0.0"
|
||||
"@dnd-kit/utilities" "^2.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/utilities@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-2.0.0.tgz#a8462dff65c6f606ecbe95273c7e263b14a1ab97"
|
||||
integrity sha512-bjs49yMNzMM+BYRsBUhTqhTk6HEvhuY3leFt6Em6NaYGgygaMbtGbbXof/UXBv7rqyyi0OkmBBnrCCcxqS2t/g==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@emotion/cache@^10.0.27":
|
||||
version "10.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
||||
@ -2008,11 +2031,6 @@
|
||||
dependencies:
|
||||
"@hapi/hoek" "^8.3.0"
|
||||
|
||||
"@interactjs/types@1.10.8":
|
||||
version "1.10.8"
|
||||
resolved "https://registry.yarnpkg.com/@interactjs/types/-/types-1.10.8.tgz#098da479de9c5ac9c8ba97d113746b7dcd9c2204"
|
||||
integrity sha512-qU2QfnN7r8AU4mSd2W3XmRtR0d35R1PReIT9b5YzpNLX9S0OQgNBLrEEFyXpa9alq/9h6wYNIwPCVAsknF5uZw==
|
||||
|
||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||
@ -7274,13 +7292,6 @@ ini@^1.3.5:
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||
|
||||
interactjs@^1.10.8:
|
||||
version "1.10.8"
|
||||
resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.10.8.tgz#a85b6e89ebf2ed88ea1678287ffcf0becf0dfb1c"
|
||||
integrity sha512-hIU82lF9mplmAHVTUmZbHMHKm96AwlD0zWGuf9krKt2dhALHsMOdU+yVilPqIv1VpNAGV66F9B14Rfs4ulS2nA==
|
||||
dependencies:
|
||||
"@interactjs/types" "1.10.8"
|
||||
|
||||
internal-ip@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"
|
||||
@ -12816,6 +12827,11 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
||||
|
||||
tsutils@^3.17.1:
|
||||
version "3.17.1"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
||||
|
Loading…
Reference in New Issue
Block a user