Move drag and drop to dnd-kit
This commit is contained in:
parent
bd522017a7
commit
8aa3bda90d
@ -5,6 +5,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babylonjs/core": "^4.2.0",
|
"@babylonjs/core": "^4.2.0",
|
||||||
"@babylonjs/loaders": "^4.2.0",
|
"@babylonjs/loaders": "^4.2.0",
|
||||||
|
"@dnd-kit/core": "3.0.0",
|
||||||
"@mitchemmc/dexie-export-import": "^1.0.1",
|
"@mitchemmc/dexie-export-import": "^1.0.1",
|
||||||
"@msgpack/msgpack": "^2.4.1",
|
"@msgpack/msgpack": "^2.4.1",
|
||||||
"@sentry/react": "^6.2.2",
|
"@sentry/react": "^6.2.2",
|
||||||
@ -25,7 +26,6 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"image-outline": "^0.1.0",
|
"image-outline": "^0.1.0",
|
||||||
"interactjs": "^1.10.8",
|
|
||||||
"konva": "^7.2.5",
|
"konva": "^7.2.5",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
|
14
src/App.js
14
src/App.js
@ -13,12 +13,8 @@ import Donate from "./routes/Donate";
|
|||||||
|
|
||||||
import { AuthProvider } from "./contexts/AuthContext";
|
import { AuthProvider } from "./contexts/AuthContext";
|
||||||
import { DatabaseProvider } from "./contexts/DatabaseContext";
|
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 { SettingsProvider } from "./contexts/SettingsContext";
|
||||||
import { KeyboardProvider } from "./contexts/KeyboardContext";
|
import { KeyboardProvider } from "./contexts/KeyboardContext";
|
||||||
import { AssetsProvider, AssetURLsProvider } from "./contexts/AssetsContext";
|
|
||||||
|
|
||||||
import { ToastProvider } from "./components/Toast";
|
import { ToastProvider } from "./components/Toast";
|
||||||
|
|
||||||
@ -49,17 +45,7 @@ function App() {
|
|||||||
<FAQ />
|
<FAQ />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/game/:id">
|
<Route path="/game/:id">
|
||||||
<AssetsProvider>
|
|
||||||
<AssetURLsProvider>
|
|
||||||
<MapLoadingProvider>
|
|
||||||
<MapDataProvider>
|
|
||||||
<TokenDataProvider>
|
|
||||||
<Game />
|
<Game />
|
||||||
</TokenDataProvider>
|
|
||||||
</MapDataProvider>
|
|
||||||
</MapLoadingProvider>
|
|
||||||
</AssetURLsProvider>
|
|
||||||
</AssetsProvider>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Home />
|
<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 React, { useEffect, useRef, useState } from "react";
|
||||||
import { Box, IconButton } from "theme-ui";
|
import { Box, IconButton } from "theme-ui";
|
||||||
|
|
||||||
import RemoveTokenIcon from "../icons/RemoveTokenIcon";
|
import RemoveTokenIcon from "../../icons/RemoveTokenIcon";
|
||||||
|
|
||||||
function DragOverlay({ dragging, node, onRemove }) {
|
function DragOverlay({ dragging, node, onRemove }) {
|
||||||
const [isRemoveHovered, setIsRemoveHovered] = useState(false);
|
const [isRemoveHovered, setIsRemoveHovered] = useState(false);
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { Box } from "theme-ui";
|
||||||
|
|
||||||
import MapControls from "./MapControls";
|
import MapControls from "./MapControls";
|
||||||
import MapInteraction from "./MapInteraction";
|
import MapInteraction from "./MapInteraction";
|
||||||
@ -18,6 +19,8 @@ import TokenDragOverlay from "../token/TokenDragOverlay";
|
|||||||
import NoteMenu from "../note/NoteMenu";
|
import NoteMenu from "../note/NoteMenu";
|
||||||
import NoteDragOverlay from "../note/NoteDragOverlay";
|
import NoteDragOverlay from "../note/NoteDragOverlay";
|
||||||
|
|
||||||
|
import Droppable from "../Droppable";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AddShapeAction,
|
AddShapeAction,
|
||||||
CutShapeAction,
|
CutShapeAction,
|
||||||
@ -336,6 +339,8 @@ function Map({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Droppable id="map" disabled={!map}>
|
||||||
<MapInteraction
|
<MapInteraction
|
||||||
map={map}
|
map={map}
|
||||||
mapState={mapState}
|
mapState={mapState}
|
||||||
@ -360,6 +365,8 @@ function Map({
|
|||||||
{mapPointer}
|
{mapPointer}
|
||||||
{mapMeasure}
|
{mapMeasure}
|
||||||
</MapInteraction>
|
</MapInteraction>
|
||||||
|
</Droppable>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,11 +181,12 @@ function MapInteraction({
|
|||||||
<GridProvider grid={map?.grid} width={mapWidth} height={mapHeight}>
|
<GridProvider grid={map?.grid} width={mapWidth} height={mapHeight}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
|
||||||
position: "relative",
|
position: "relative",
|
||||||
cursor: getCursorForTool(selectedToolId),
|
cursor: getCursorForTool(selectedToolId),
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="map"
|
className="map"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import DragOverlay from "../DragOverlay";
|
import DragOverlay from "../map/DragOverlay";
|
||||||
|
|
||||||
function NoteDragOverlay({ onNoteRemove, noteId, noteGroup, dragging }) {
|
function NoteDragOverlay({ onNoteRemove, noteId, noteGroup, dragging }) {
|
||||||
function handleNoteRemove() {
|
function handleNoteRemove() {
|
||||||
|
@ -7,7 +7,7 @@ import { useDataURL } from "../../contexts/AssetsContext";
|
|||||||
|
|
||||||
import { tokenSources, unknownSource } from "../../tokens";
|
import { tokenSources, unknownSource } from "../../tokens";
|
||||||
|
|
||||||
function ListToken({ token, className }) {
|
function ListToken({ token }) {
|
||||||
const tokenURL = useDataURL(
|
const tokenURL = useDataURL(
|
||||||
token,
|
token,
|
||||||
tokenSources,
|
tokenSources,
|
||||||
@ -20,11 +20,10 @@ function ListToken({ token, className }) {
|
|||||||
usePreventTouch(imageRef);
|
usePreventTouch(imageRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box my={2} mx={3} sx={{ width: "48px", height: "48px" }}>
|
<Box py={1} sx={{ width: "48px", height: "56px" }}>
|
||||||
<Image
|
<Image
|
||||||
src={tokenURL}
|
src={tokenURL}
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
className={className}
|
|
||||||
sx={{
|
sx={{
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
@ -32,8 +31,6 @@ function ListToken({ token, className }) {
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
// pass id into the dom element which is then used by the ProxyToken
|
|
||||||
data-id={token.id}
|
|
||||||
alt={token.name}
|
alt={token.name}
|
||||||
title={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,79 +1,52 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { Box, Flex } from "theme-ui";
|
import { Box, Flex } from "theme-ui";
|
||||||
import shortid from "shortid";
|
|
||||||
import SimpleBar from "simplebar-react";
|
import SimpleBar from "simplebar-react";
|
||||||
|
import { DragOverlay } from "@dnd-kit/core";
|
||||||
|
|
||||||
import ListToken from "./ListToken";
|
import ListToken from "./ListToken";
|
||||||
import ProxyToken from "./ProxyToken";
|
|
||||||
|
|
||||||
import SelectTokensButton from "./SelectTokensButton";
|
import SelectTokensButton from "./SelectTokensButton";
|
||||||
|
import Draggable from "../Draggable";
|
||||||
import { fromEntries } from "../../helpers/shared";
|
|
||||||
|
|
||||||
import useSetting from "../../hooks/useSetting";
|
import useSetting from "../../hooks/useSetting";
|
||||||
|
|
||||||
import { useAuth } from "../../contexts/AuthContext";
|
|
||||||
import { useTokenData } from "../../contexts/TokenDataContext";
|
import { useTokenData } from "../../contexts/TokenDataContext";
|
||||||
|
import { useDragId } from "../../contexts/DragContext";
|
||||||
|
|
||||||
const listTokenClassName = "list-token";
|
function TokenBar() {
|
||||||
|
const { ownedTokens } = useTokenData();
|
||||||
function TokenBar({ onMapTokenStateCreate }) {
|
|
||||||
const { userId } = useAuth();
|
|
||||||
const { ownedTokens, tokens } = useTokenData();
|
|
||||||
const [fullScreen] = useSetting("map.fullScreen");
|
const [fullScreen] = useSetting("map.fullScreen");
|
||||||
|
|
||||||
function handleProxyDragEnd(isOnMap, token) {
|
const activeDragId = useDragId();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "80px",
|
width: "80px",
|
||||||
minWidth: "80px",
|
minWidth: "80px",
|
||||||
overflow: "hidden",
|
overflowY: "scroll",
|
||||||
|
overflowX: "hidden",
|
||||||
display: fullScreen ? "none" : "block",
|
display: fullScreen ? "none" : "block",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SimpleBar style={{ height: "calc(100% - 48px)", overflowX: "hidden" }}>
|
<SimpleBar
|
||||||
|
style={{
|
||||||
|
height: "calc(100% - 48px)",
|
||||||
|
overflowX: "hidden",
|
||||||
|
padding: "0 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{ownedTokens
|
{ownedTokens
|
||||||
.filter((token) => !token.hideInSidebar)
|
.filter((token) => !token.hideInSidebar)
|
||||||
.map((token) => (
|
.map((token) => (
|
||||||
<ListToken
|
<Draggable
|
||||||
|
id={`sidebar-${token.id}`}
|
||||||
key={token.id}
|
key={token.id}
|
||||||
token={token}
|
data={{ tokenId: token.id }}
|
||||||
className={listTokenClassName}
|
>
|
||||||
/>
|
<ListToken token={token} />
|
||||||
|
</Draggable>
|
||||||
))}
|
))}
|
||||||
</SimpleBar>
|
</SimpleBar>
|
||||||
<Flex
|
<Flex
|
||||||
@ -86,13 +59,19 @@ function TokenBar({ onMapTokenStateCreate }) {
|
|||||||
>
|
>
|
||||||
<SelectTokensButton />
|
<SelectTokensButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
{createPortal(
|
||||||
<ProxyToken
|
<DragOverlay>
|
||||||
tokenClassName={listTokenClassName}
|
{activeDragId && (
|
||||||
onProxyDragEnd={handleProxyDragEnd}
|
<ListToken
|
||||||
tokens={fromEntries(tokens.map((token) => [token.id, token]))}
|
token={ownedTokens.find(
|
||||||
|
(token) => `sidebar-${token.id}` === activeDragId
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
)}
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
useMapHeight,
|
useMapHeight,
|
||||||
} from "../../contexts/MapInteractionContext";
|
} from "../../contexts/MapInteractionContext";
|
||||||
|
|
||||||
import DragOverlay from "../DragOverlay";
|
import DragOverlay from "../map/DragOverlay";
|
||||||
|
|
||||||
function TokenDragOverlay({
|
function TokenDragOverlay({
|
||||||
onTokenStateRemove,
|
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 React, { useState, useEffect, useRef } from "react";
|
||||||
import { useToasts } from "react-toast-notifications";
|
import { useToasts } from "react-toast-notifications";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
import { useMapData } from "../contexts/MapDataContext";
|
import { useMapData } from "../contexts/MapDataContext";
|
||||||
import { useMapLoading } from "../contexts/MapLoadingContext";
|
import { useMapLoading } from "../contexts/MapLoadingContext";
|
||||||
@ -7,6 +8,9 @@ import { useAuth } from "../contexts/AuthContext";
|
|||||||
import { useDatabase } from "../contexts/DatabaseContext";
|
import { useDatabase } from "../contexts/DatabaseContext";
|
||||||
import { useParty } from "../contexts/PartyContext";
|
import { useParty } from "../contexts/PartyContext";
|
||||||
import { useAssets } from "../contexts/AssetsContext";
|
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";
|
import { omit } from "../helpers/shared";
|
||||||
|
|
||||||
@ -40,8 +44,10 @@ function NetworkedMapAndTokens({ session }) {
|
|||||||
const { userId } = useAuth();
|
const { userId } = useAuth();
|
||||||
const partyState = useParty();
|
const partyState = useParty();
|
||||||
const { assetLoadStart, assetProgressUpdate, isLoading } = useMapLoading();
|
const { assetLoadStart, assetProgressUpdate, isLoading } = useMapLoading();
|
||||||
|
const mapStageRef = useMapStage();
|
||||||
|
|
||||||
const { updateMapState } = useMapData();
|
const { updateMapState } = useMapData();
|
||||||
|
const { tokensById } = useTokenData();
|
||||||
const { getAsset, putAsset } = useAssets();
|
const { getAsset, putAsset } = useAssets();
|
||||||
|
|
||||||
const [currentMap, setCurrentMap] = useState(null);
|
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(() => {
|
useEffect(() => {
|
||||||
async function handlePeerData({ id, data, reply }) {
|
async function handlePeerData({ id, data, reply }) {
|
||||||
if (id === "assetRequest") {
|
if (id === "assetRequest") {
|
||||||
@ -451,7 +504,7 @@ function NetworkedMapAndTokens({ session }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DragProvider onDragEnd={handleDragEnd}>
|
||||||
<Map
|
<Map
|
||||||
map={currentMap}
|
map={currentMap}
|
||||||
mapState={currentMapState}
|
mapState={currentMapState}
|
||||||
@ -475,8 +528,8 @@ function NetworkedMapAndTokens({ session }) {
|
|||||||
disabledTokens={disabledMapTokens}
|
disabledTokens={disabledMapTokens}
|
||||||
session={session}
|
session={session}
|
||||||
/>
|
/>
|
||||||
<TokenBar onMapTokenStateCreate={handleMapTokenStateCreate} />
|
<TokenBar />
|
||||||
</>
|
</DragProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,10 @@ import { MapStageProvider } from "../contexts/MapStageContext";
|
|||||||
import { useDatabase } from "../contexts/DatabaseContext";
|
import { useDatabase } from "../contexts/DatabaseContext";
|
||||||
import { PlayerProvider } from "../contexts/PlayerContext";
|
import { PlayerProvider } from "../contexts/PlayerContext";
|
||||||
import { PartyProvider } from "../contexts/PartyContext";
|
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 NetworkedMapAndTokens from "../network/NetworkedMapAndTokens";
|
||||||
import NetworkedParty from "../network/NetworkedParty";
|
import NetworkedParty from "../network/NetworkedParty";
|
||||||
@ -84,7 +88,6 @@ function Game() {
|
|||||||
};
|
};
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
|
|
||||||
// Join game
|
// Join game
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionStatus === "ready" && databaseStatus !== "loading") {
|
if (sessionStatus === "ready" && databaseStatus !== "loading") {
|
||||||
@ -103,6 +106,11 @@ function Game() {
|
|||||||
const mapStageRef = useRef();
|
const mapStageRef = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AssetsProvider>
|
||||||
|
<AssetURLsProvider>
|
||||||
|
<MapLoadingProvider>
|
||||||
|
<MapDataProvider>
|
||||||
|
<TokenDataProvider>
|
||||||
<PlayerProvider session={session}>
|
<PlayerProvider session={session}>
|
||||||
<PartyProvider session={session}>
|
<PartyProvider session={session}>
|
||||||
<MapStageProvider value={mapStageRef}>
|
<MapStageProvider value={mapStageRef}>
|
||||||
@ -124,13 +132,15 @@ function Game() {
|
|||||||
>
|
>
|
||||||
<Box p={1}>
|
<Box p={1}>
|
||||||
<Text as="p" variant="body2">
|
<Text as="p" variant="body2">
|
||||||
{peerError} See <Link to="/faq#connection">FAQ</Link> for more
|
{peerError} See <Link to="/faq#connection">FAQ</Link>{" "}
|
||||||
information.
|
for more information.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Banner>
|
</Banner>
|
||||||
<OfflineBanner isOpen={sessionStatus === "offline"} />
|
<OfflineBanner isOpen={sessionStatus === "offline"} />
|
||||||
<ReconnectBanner isOpen={sessionStatus === "reconnecting"} />
|
<ReconnectBanner
|
||||||
|
isOpen={sessionStatus === "reconnecting"}
|
||||||
|
/>
|
||||||
<AuthModal
|
<AuthModal
|
||||||
isOpen={sessionStatus === "auth"}
|
isOpen={sessionStatus === "auth"}
|
||||||
onSubmit={handleAuthSubmit}
|
onSubmit={handleAuthSubmit}
|
||||||
@ -147,6 +157,11 @@ function Game() {
|
|||||||
</MapStageProvider>
|
</MapStageProvider>
|
||||||
</PartyProvider>
|
</PartyProvider>
|
||||||
</PlayerProvider>
|
</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"
|
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
|
||||||
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
|
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":
|
"@emotion/cache@^10.0.27":
|
||||||
version "10.0.29"
|
version "10.0.29"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
||||||
@ -2008,11 +2031,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@hapi/hoek" "^8.3.0"
|
"@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":
|
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
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"
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
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:
|
internal-ip@^4.3.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"
|
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"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
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:
|
tsutils@^3.17.1:
|
||||||
version "3.17.1"
|
version "3.17.1"
|
||||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
||||||
|
Loading…
Reference in New Issue
Block a user