Added initial token label
Added token pop up menu Added token label Added better token positioning Split tokens into list and map variants Moved size input to more generic number input Changed game handler names to be more consistent
This commit is contained in:
parent
da0f80316c
commit
cb93922d59
21
src/components/ListToken.js
Normal file
21
src/components/ListToken.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
import { Image } from "theme-ui";
|
||||||
|
|
||||||
|
import usePreventTouch from "../helpers/usePreventTouch";
|
||||||
|
|
||||||
|
function ListToken({ image, className }) {
|
||||||
|
const imageRef = useRef();
|
||||||
|
// Stop touch to prevent 3d touch gesutre on iOS
|
||||||
|
usePreventTouch(imageRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
ref={imageRef}
|
||||||
|
className={className}
|
||||||
|
sx={{ userSelect: "none", touchAction: "none" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListToken;
|
@ -2,9 +2,10 @@ import React, { useRef, useEffect, useState } from "react";
|
|||||||
import { Box, Image } from "theme-ui";
|
import { Box, Image } from "theme-ui";
|
||||||
import interact from "interactjs";
|
import interact from "interactjs";
|
||||||
|
|
||||||
import Token from "../components/Token";
|
|
||||||
import ProxyToken from "../components/ProxyToken";
|
import ProxyToken from "../components/ProxyToken";
|
||||||
import AddMapButton from "../components/AddMapButton";
|
import AddMapButton from "../components/AddMapButton";
|
||||||
|
import TokenMenu from "../components/TokenMenu";
|
||||||
|
import MapToken from "../components/MapToken";
|
||||||
|
|
||||||
const mapTokenClassName = "map-token";
|
const mapTokenClassName = "map-token";
|
||||||
const zoomSpeed = -0.005;
|
const zoomSpeed = -0.005;
|
||||||
@ -15,13 +16,13 @@ function Map({
|
|||||||
mapSource,
|
mapSource,
|
||||||
mapData,
|
mapData,
|
||||||
tokens,
|
tokens,
|
||||||
onMapTokenMove,
|
onMapTokenChange,
|
||||||
onMapTokenRemove,
|
onMapTokenRemove,
|
||||||
onMapChanged,
|
onMapChange,
|
||||||
}) {
|
}) {
|
||||||
function handleProxyDragEnd(isOnMap, token) {
|
function handleProxyDragEnd(isOnMap, token) {
|
||||||
if (isOnMap && onMapTokenMove) {
|
if (isOnMap && onMapTokenChange) {
|
||||||
onMapTokenMove(token);
|
onMapTokenChange(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOnMap && onMapTokenRemove) {
|
if (!isOnMap && onMapTokenRemove) {
|
||||||
@ -120,6 +121,64 @@ function Map({
|
|||||||
const tokenSizePercent = (1 / rows) * 100;
|
const tokenSizePercent = (1 / rows) * 100;
|
||||||
const aspectRatio = (mapData && mapData.width / mapData.height) || 1;
|
const aspectRatio = (mapData && mapData.width / mapData.height) || 1;
|
||||||
|
|
||||||
|
const mapImage = (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
ref={mapRef}
|
||||||
|
id="map"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
userSelect: "none",
|
||||||
|
touchAction: "none",
|
||||||
|
}}
|
||||||
|
src={mapSource}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapTokens = (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.values(tokens).map((token) => (
|
||||||
|
<MapToken
|
||||||
|
key={token.id}
|
||||||
|
token={token}
|
||||||
|
tokenSizePercent={tokenSizePercent}
|
||||||
|
className={mapTokenClassName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapActions = (
|
||||||
|
<Box
|
||||||
|
p={2}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "0",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddMapButton onMapChanged={onMapChange} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
@ -155,78 +214,20 @@ function Map({
|
|||||||
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box
|
{mapImage}
|
||||||
sx={{
|
{mapTokens}
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
ref={mapRef}
|
|
||||||
id="map"
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
userSelect: "none",
|
|
||||||
touchAction: "none",
|
|
||||||
}}
|
|
||||||
src={mapSource}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.values(tokens).map((token) => (
|
|
||||||
<Box
|
|
||||||
key={token.id}
|
|
||||||
style={{
|
|
||||||
left: `${token.x * 100}%`,
|
|
||||||
top: `${token.y * 100}%`,
|
|
||||||
width: `${tokenSizePercent * (token.size || 1)}%`,
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Token
|
|
||||||
data={{
|
|
||||||
id: token.id,
|
|
||||||
size: token.size,
|
|
||||||
}}
|
|
||||||
image={token.image}
|
|
||||||
className={mapTokenClassName}
|
|
||||||
sx={{ position: "absolute" }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
{mapActions}
|
||||||
p={2}
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "0",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AddMapButton onMapChanged={onMapChanged} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
<ProxyToken
|
<ProxyToken
|
||||||
tokenClassName={mapTokenClassName}
|
tokenClassName={mapTokenClassName}
|
||||||
onProxyDragEnd={handleProxyDragEnd}
|
onProxyDragEnd={handleProxyDragEnd}
|
||||||
/>
|
/>
|
||||||
|
<TokenMenu
|
||||||
|
tokenClassName={mapTokenClassName}
|
||||||
|
onTokenChange={onMapTokenChange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
57
src/components/MapToken.js
Normal file
57
src/components/MapToken.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
import { Box, Image } from "theme-ui";
|
||||||
|
|
||||||
|
import TokenLabel from "./TokenLabel";
|
||||||
|
|
||||||
|
import usePreventTouch from "../helpers/usePreventTouch";
|
||||||
|
|
||||||
|
function MapToken({ token, tokenSizePercent, className }) {
|
||||||
|
const imageRef = useRef();
|
||||||
|
// Stop touch to prevent 3d touch gesutre on iOS
|
||||||
|
usePreventTouch(imageRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
transform: `translate(${token.x * 100}%, ${token.y * 100}%)`,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: `${tokenSizePercent * (token.size || 1)}%`,
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
pointerEvents: "all",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ position: "absolute", display: "flex", width: "100%" }}>
|
||||||
|
<Image
|
||||||
|
className={className}
|
||||||
|
sx={{
|
||||||
|
userSelect: "none",
|
||||||
|
touchAction: "none",
|
||||||
|
width: "100%",
|
||||||
|
position: "absolute", // Fix image stretch in safari
|
||||||
|
}}
|
||||||
|
src={token.image}
|
||||||
|
// pass data into the dom element used to pass state to the ProxyToken
|
||||||
|
data-id={token.id}
|
||||||
|
data-size={token.size}
|
||||||
|
data-label={token.label}
|
||||||
|
ref={imageRef}
|
||||||
|
/>
|
||||||
|
{token.label && <TokenLabel label={token.label} />}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapToken;
|
@ -1,17 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, Flex, IconButton, Text } from "theme-ui";
|
import { Box, Flex, IconButton, Text } from "theme-ui";
|
||||||
|
|
||||||
function SizeInput({ value, onChange }) {
|
function NumberInput({ value, onChange, title, min, max }) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Text sx={{ textAlign: "center" }} variant="heading" as="h1">
|
<Text sx={{ textAlign: "center" }} variant="heading" as="h1">
|
||||||
Size
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Flex sx={{ alignItems: "center", justifyContent: "center" }}>
|
<Flex sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Lower Token Size"
|
aria-label={`Decrease ${title}`}
|
||||||
title="Lower Token Size"
|
title={`Decrease ${title}`}
|
||||||
onClick={() => value > 1 && onChange(value - 1)}
|
onClick={() => value > min && onChange(value - 1)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -24,13 +24,13 @@ function SizeInput({ value, onChange }) {
|
|||||||
<path d="M18 13H6c-.55 0-1-.45-1-1s.45-1 1-1h12c.55 0 1 .45 1 1s-.45 1-1 1z" />
|
<path d="M18 13H6c-.55 0-1-.45-1-1s.45-1 1-1h12c.55 0 1 .45 1 1s-.45 1-1 1z" />
|
||||||
</svg>
|
</svg>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Text as="p" aria-label="Current Token Size">
|
<Text as="p" aria-label={`Current ${title}`}>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Increase Token Size"
|
aria-label={`Increase ${title}`}
|
||||||
title="Increase Token Size"
|
title={`Increase ${title}`}
|
||||||
onClick={() => onChange(value + 1)}
|
onClick={() => value < max && onChange(value + 1)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -48,9 +48,12 @@ function SizeInput({ value, onChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SizeInput.defaultProps = {
|
NumberInput.defaultProps = {
|
||||||
value: 1,
|
value: 1,
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
|
title: "Number",
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SizeInput;
|
export default NumberInput;
|
@ -86,10 +86,11 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
|
|||||||
x = x / (mapRect.right - mapRect.left);
|
x = x / (mapRect.right - mapRect.left);
|
||||||
y = y / (mapRect.bottom - mapRect.top);
|
y = y / (mapRect.bottom - mapRect.top);
|
||||||
|
|
||||||
|
target.setAttribute("data-x", x);
|
||||||
|
target.setAttribute("data-y", y);
|
||||||
|
|
||||||
onProxyDragEnd(proxyOnMap.current, {
|
onProxyDragEnd(proxyOnMap.current, {
|
||||||
image: imageSource,
|
image: imageSource,
|
||||||
x,
|
|
||||||
y,
|
|
||||||
// Pass in props stored as data- in the dom node
|
// Pass in props stored as data- in the dom node
|
||||||
...target.dataset,
|
...target.dataset,
|
||||||
});
|
});
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Image } from "theme-ui";
|
|
||||||
|
|
||||||
import { fromEntries } from "../helpers/shared";
|
|
||||||
|
|
||||||
// The data prop is used to pass data into the dom element
|
|
||||||
// this can be used to pass state to the ProxyToken
|
|
||||||
function Token({ image, className, data, sx }) {
|
|
||||||
// Map the keys in data to have the `data-` prefix
|
|
||||||
const dataProps = fromEntries(
|
|
||||||
Object.entries(data).map(([key, value]) => [`data-${key}`, value])
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
className={className}
|
|
||||||
sx={{ userSelect: "none", touchAction: "none", ...sx }}
|
|
||||||
src={image}
|
|
||||||
{...dataProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Token.defaultProps = {
|
|
||||||
data: {},
|
|
||||||
sx: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Token;
|
|
48
src/components/TokenLabel.js
Normal file
48
src/components/TokenLabel.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Image, Box, Text } from "theme-ui";
|
||||||
|
|
||||||
|
import tokenLabel from "../images/TokenLabel.png";
|
||||||
|
|
||||||
|
function TokenLabel({ label }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
transform: "scale(0.3) translate(0, 20%)",
|
||||||
|
transformOrigin: "bottom center",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image src={tokenLabel} />
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<foreignObject width="100%" height="100%">
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
variant="heading"
|
||||||
|
sx={{
|
||||||
|
// This value is actually 66%
|
||||||
|
fontSize: "66px",
|
||||||
|
width: "100px",
|
||||||
|
height: "100px",
|
||||||
|
textAlign: "center",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</foreignObject>
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TokenLabel;
|
129
src/components/TokenMenu.js
Normal file
129
src/components/TokenMenu.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Modal from "react-modal";
|
||||||
|
import interact from "interactjs";
|
||||||
|
import { useThemeUI, Box, Input } from "theme-ui";
|
||||||
|
|
||||||
|
function TokenMenu({ tokenClassName, onTokenChange }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
function handleRequestClose() {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [currentToken, setCurrentToken] = useState(0);
|
||||||
|
const [menuLeft, setMenuLeft] = useState(0);
|
||||||
|
const [menuTop, setMenuTop] = useState(0);
|
||||||
|
|
||||||
|
function handleLabelChange(event) {
|
||||||
|
// Slice to remove Label: text
|
||||||
|
const label = event.target.value.slice(7);
|
||||||
|
if (label.length <= 1) {
|
||||||
|
setCurrentToken((prevToken) => ({
|
||||||
|
...prevToken,
|
||||||
|
label: label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
onTokenChange({ ...currentToken, label: label });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleTokenMenuOpen(event) {
|
||||||
|
const target = event.target;
|
||||||
|
const dataset = (target && target.dataset) || {};
|
||||||
|
setCurrentToken({
|
||||||
|
image: target.src,
|
||||||
|
...dataset,
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
setMenuLeft(targetRect.left);
|
||||||
|
setMenuTop(targetRect.bottom);
|
||||||
|
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add listener for hold gesture
|
||||||
|
interact(`.${tokenClassName}`).on("hold", handleTokenMenuOpen);
|
||||||
|
|
||||||
|
function handleMapContextMenu(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.target.classList.contains(tokenClassName)) {
|
||||||
|
handleTokenMenuOpen(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle context menu on the map level as handling
|
||||||
|
// on the token level lead to the default menu still
|
||||||
|
// being displayed
|
||||||
|
const map = document.querySelector(".map");
|
||||||
|
map.addEventListener("contextmenu", handleMapContextMenu);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
map.removeEventListener("contextmenu", handleMapContextMenu);
|
||||||
|
};
|
||||||
|
}, [tokenClassName]);
|
||||||
|
|
||||||
|
const { theme } = useThemeUI();
|
||||||
|
|
||||||
|
function handleModalContent(node) {
|
||||||
|
if (node) {
|
||||||
|
console.log(node);
|
||||||
|
const tokenLabelInput = node.querySelector("#changeTokenLabel");
|
||||||
|
tokenLabelInput.focus();
|
||||||
|
// Highlight label section of input
|
||||||
|
tokenLabelInput.setSelectionRange(7, 8);
|
||||||
|
tokenLabelInput.onblur = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
// Check for wheel event to close modal as well
|
||||||
|
document.body.addEventListener(
|
||||||
|
"wheel",
|
||||||
|
() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onRequestClose={handleRequestClose}
|
||||||
|
style={{
|
||||||
|
overlay: { top: "0", bottom: "initial" },
|
||||||
|
content: {
|
||||||
|
backgroundColor: theme.colors.highlight,
|
||||||
|
top: `${menuTop}px`,
|
||||||
|
left: `${menuLeft}px`,
|
||||||
|
right: "initial",
|
||||||
|
bottom: "initial",
|
||||||
|
padding: 0,
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
contentRef={handleModalContent}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
as="form"
|
||||||
|
bg="background"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRequestClose();
|
||||||
|
}}
|
||||||
|
sx={{ width: "72px" }}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="changeTokenLabel"
|
||||||
|
onChange={handleLabelChange}
|
||||||
|
value={`Label: ${currentToken.label}`}
|
||||||
|
sx={{ padding: "4px" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TokenMenu;
|
@ -5,9 +5,9 @@ import SimpleBar from "simplebar-react";
|
|||||||
|
|
||||||
import * as tokens from "../tokens";
|
import * as tokens from "../tokens";
|
||||||
|
|
||||||
import Token from "./Token";
|
import ListToken from "./ListToken";
|
||||||
import ProxyToken from "./ProxyToken";
|
import ProxyToken from "./ProxyToken";
|
||||||
import SizeInput from "./SizeInput";
|
import NumberInput from "./NumberInput";
|
||||||
|
|
||||||
const listTokenClassName = "list-token";
|
const listTokenClassName = "list-token";
|
||||||
|
|
||||||
@ -17,7 +17,12 @@ function Tokens({ onCreateMapToken }) {
|
|||||||
function handleProxyDragEnd(isOnMap, token) {
|
function handleProxyDragEnd(isOnMap, token) {
|
||||||
if (isOnMap && onCreateMapToken) {
|
if (isOnMap && onCreateMapToken) {
|
||||||
// Give the token an id
|
// Give the token an id
|
||||||
onCreateMapToken({ ...token, id: shortid.generate(), size: tokenSize });
|
onCreateMapToken({
|
||||||
|
...token,
|
||||||
|
id: shortid.generate(),
|
||||||
|
size: tokenSize,
|
||||||
|
label: "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,12 +39,18 @@ function Tokens({ onCreateMapToken }) {
|
|||||||
<SimpleBar style={{ height: "calc(100% - 58px)", overflowX: "hidden" }}>
|
<SimpleBar style={{ height: "calc(100% - 58px)", overflowX: "hidden" }}>
|
||||||
{Object.entries(tokens).map(([id, image]) => (
|
{Object.entries(tokens).map(([id, image]) => (
|
||||||
<Box key={id} my={2} mx={3} sx={{ width: "48px", height: "48px" }}>
|
<Box key={id} my={2} mx={3} sx={{ width: "48px", height: "48px" }}>
|
||||||
<Token image={image} className={listTokenClassName} />
|
<ListToken image={image} className={listTokenClassName} />
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleBar>
|
</SimpleBar>
|
||||||
<Box pt={1} bg="muted" sx={{ height: "58px" }}>
|
<Box pt={1} bg="muted" sx={{ height: "58px" }}>
|
||||||
<SizeInput value={tokenSize} onChange={setTokenSize} />
|
<NumberInput
|
||||||
|
value={tokenSize}
|
||||||
|
onChange={setTokenSize}
|
||||||
|
title="Size"
|
||||||
|
min={1}
|
||||||
|
max={9}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ProxyToken
|
<ProxyToken
|
||||||
|
22
src/helpers/usePreventTouch.js
Normal file
22
src/helpers/usePreventTouch.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
function usePreventTouch(elementRef) {
|
||||||
|
useEffect(() => {
|
||||||
|
// Stop 3d touch
|
||||||
|
function prevent3DTouch(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
const element = elementRef.current;
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener("touchstart", prevent3DTouch, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (element) {
|
||||||
|
element.removeEventListener("touchstart", prevent3DTouch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [elementRef.current]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePreventTouch;
|
BIN
src/images/TokenLabel.png
Normal file
BIN
src/images/TokenLabel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
@ -28,7 +28,7 @@ function Game() {
|
|||||||
const [mapSource, setMapSource] = useState(null);
|
const [mapSource, setMapSource] = useState(null);
|
||||||
const mapDataRef = useRef(null);
|
const mapDataRef = useRef(null);
|
||||||
|
|
||||||
function handleMapChanged(mapData, mapSource) {
|
function handleMapChange(mapData, mapSource) {
|
||||||
mapDataRef.current = mapData;
|
mapDataRef.current = mapData;
|
||||||
setMapSource(mapSource);
|
setMapSource(mapSource);
|
||||||
for (let peer of Object.values(peers)) {
|
for (let peer of Object.values(peers)) {
|
||||||
@ -38,7 +38,7 @@ function Game() {
|
|||||||
|
|
||||||
const [mapTokens, setMapTokens] = useState({});
|
const [mapTokens, setMapTokens] = useState({});
|
||||||
|
|
||||||
function handleEditMapToken(token) {
|
function handleMapTokenChange(token) {
|
||||||
if (!mapSource) {
|
if (!mapSource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ function Game() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemoveMapToken(token) {
|
function handleMapTokenRemove(token) {
|
||||||
setMapTokens((prevMapTokens) => {
|
setMapTokens((prevMapTokens) => {
|
||||||
const { [token.id]: old, ...rest } = prevMapTokens;
|
const { [token.id]: old, ...rest } = prevMapTokens;
|
||||||
return rest;
|
return rest;
|
||||||
@ -213,11 +213,11 @@ function Game() {
|
|||||||
mapSource={mapSource}
|
mapSource={mapSource}
|
||||||
mapData={mapDataRef.current}
|
mapData={mapDataRef.current}
|
||||||
tokens={mapTokens}
|
tokens={mapTokens}
|
||||||
onMapTokenMove={handleEditMapToken}
|
onMapTokenChange={handleMapTokenChange}
|
||||||
onMapTokenRemove={handleRemoveMapToken}
|
onMapTokenRemove={handleMapTokenRemove}
|
||||||
onMapChanged={handleMapChanged}
|
onMapChange={handleMapChange}
|
||||||
/>
|
/>
|
||||||
<Tokens onCreateMapToken={handleEditMapToken} />
|
<Tokens onCreateMapToken={handleMapTokenChange} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Banner isOpen={!!peerError} onRequestClose={() => setPeerError(null)}>
|
<Banner isOpen={!!peerError} onRequestClose={() => setPeerError(null)}>
|
||||||
|
Loading…
Reference in New Issue
Block a user