2020-05-22 13:46:52 +10:00
|
|
|
import React, { useEffect, useState } from "react";
|
2020-05-20 15:44:28 +10:00
|
|
|
import { Box, Input, Slider, Flex, Text } from "theme-ui";
|
2020-04-13 23:42:18 +10:00
|
|
|
|
2020-04-23 10:09:12 +10:00
|
|
|
import MapMenu from "../map/MapMenu";
|
2020-04-20 10:56:51 +10:00
|
|
|
|
2020-04-23 10:09:12 +10:00
|
|
|
import colors, { colorOptions } from "../../helpers/colors";
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
import usePrevious from "../../helpers/usePrevious";
|
|
|
|
|
|
2020-05-20 15:44:28 +10:00
|
|
|
const defaultTokenMaxSize = 6;
|
|
|
|
|
|
2020-04-24 15:50:05 +10:00
|
|
|
/**
|
|
|
|
|
* @callback onTokenChange
|
|
|
|
|
* @param {Object} token the token that was changed
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {string} tokenClassName The class name to attach the interactjs handler to
|
|
|
|
|
* @param {onProxyDragEnd} onTokenChange Called when the the token data is changed
|
|
|
|
|
* @param {Object} tokens An mapping of tokens to use as a base when calling onTokenChange
|
2020-04-30 15:12:34 +10:00
|
|
|
* @param {Object} disabledTokens An optional mapping of tokens that shouldn't allow interaction
|
2020-04-24 15:50:05 +10:00
|
|
|
*/
|
2020-05-21 16:46:50 +10:00
|
|
|
function TokenMenu({
|
|
|
|
|
isOpen,
|
|
|
|
|
onRequestClose,
|
|
|
|
|
tokenState,
|
|
|
|
|
tokenImage,
|
|
|
|
|
onTokenChange,
|
|
|
|
|
}) {
|
|
|
|
|
const wasOpen = usePrevious(isOpen);
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
const [tokenMaxSize, setTokenMaxSize] = useState(defaultTokenMaxSize);
|
2020-04-24 15:50:05 +10:00
|
|
|
useEffect(() => {
|
2020-05-22 13:46:52 +10:00
|
|
|
if (isOpen && !wasOpen && tokenState) {
|
2020-05-21 16:46:50 +10:00
|
|
|
setTokenMaxSize(Math.max(tokenState.size, defaultTokenMaxSize));
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen, tokenState, wasOpen]);
|
|
|
|
|
|
|
|
|
|
function handleLabelChange(event) {
|
|
|
|
|
const label = event.target.value;
|
|
|
|
|
onTokenChange({ ...tokenState, label: label });
|
|
|
|
|
}
|
2020-04-24 15:50:05 +10:00
|
|
|
|
2020-04-13 00:24:03 +10:00
|
|
|
const [menuLeft, setMenuLeft] = useState(0);
|
|
|
|
|
const [menuTop, setMenuTop] = useState(0);
|
|
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (tokenImage) {
|
|
|
|
|
const imageRect = tokenImage.getClientRect();
|
|
|
|
|
const map = document.querySelector(".map");
|
|
|
|
|
const mapRect = map.getBoundingClientRect();
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
// Center X for the menu which is 156px wide
|
|
|
|
|
setMenuLeft(mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2);
|
|
|
|
|
// Y 12px from the bottom
|
|
|
|
|
setMenuTop(mapRect.top + imageRect.y + imageRect.height + 12);
|
|
|
|
|
}
|
|
|
|
|
}, [tokenImage]);
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2020-04-13 23:42:18 +10:00
|
|
|
function handleStatusChange(status) {
|
2020-05-21 16:46:50 +10:00
|
|
|
const statuses = tokenState.statuses;
|
2020-04-13 23:42:18 +10:00
|
|
|
let newStatuses = [];
|
|
|
|
|
if (statuses.includes(status)) {
|
|
|
|
|
newStatuses = statuses.filter((s) => s !== status);
|
|
|
|
|
} else {
|
|
|
|
|
newStatuses = [...statuses, status];
|
|
|
|
|
}
|
2020-05-21 16:46:50 +10:00
|
|
|
onTokenChange({ ...tokenState, statuses: newStatuses });
|
2020-04-13 23:42:18 +10:00
|
|
|
}
|
|
|
|
|
|
2020-05-20 15:44:28 +10:00
|
|
|
function handleSizeChange(event) {
|
|
|
|
|
const newSize = parseInt(event.target.value);
|
2020-05-21 16:46:50 +10:00
|
|
|
onTokenChange({ ...tokenState, size: newSize });
|
2020-05-20 15:44:28 +10:00
|
|
|
}
|
|
|
|
|
|
2020-04-13 00:24:03 +10:00
|
|
|
function handleModalContent(node) {
|
|
|
|
|
if (node) {
|
2020-04-20 10:56:51 +10:00
|
|
|
// Focus input
|
2020-04-13 00:24:03 +10:00
|
|
|
const tokenLabelInput = node.querySelector("#changeTokenLabel");
|
|
|
|
|
tokenLabelInput.focus();
|
2020-05-20 15:44:28 +10:00
|
|
|
tokenLabelInput.select();
|
2020-04-13 23:42:18 +10:00
|
|
|
|
2020-04-14 09:41:10 +10:00
|
|
|
// Ensure menu is in bounds
|
|
|
|
|
const nodeRect = node.getBoundingClientRect();
|
|
|
|
|
const map = document.querySelector(".map");
|
|
|
|
|
const mapRect = map.getBoundingClientRect();
|
|
|
|
|
setMenuLeft((prevLeft) =>
|
|
|
|
|
Math.min(
|
|
|
|
|
mapRect.right - nodeRect.width,
|
|
|
|
|
Math.max(mapRect.left, prevLeft)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
setMenuTop((prevTop) =>
|
|
|
|
|
Math.min(mapRect.bottom - nodeRect.height, prevTop)
|
|
|
|
|
);
|
2020-04-13 00:24:03 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2020-04-20 10:56:51 +10:00
|
|
|
<MapMenu
|
2020-04-13 00:24:03 +10:00
|
|
|
isOpen={isOpen}
|
2020-05-21 16:46:50 +10:00
|
|
|
onRequestClose={onRequestClose}
|
2020-04-20 10:56:51 +10:00
|
|
|
top={`${menuTop}px`}
|
|
|
|
|
left={`${menuLeft}px`}
|
|
|
|
|
onModalContent={handleModalContent}
|
2020-04-13 00:24:03 +10:00
|
|
|
>
|
2020-05-20 15:44:28 +10:00
|
|
|
<Box sx={{ width: "156px" }} p={1}>
|
|
|
|
|
<Flex
|
2020-04-13 23:42:18 +10:00
|
|
|
as="form"
|
|
|
|
|
onSubmit={(e) => {
|
|
|
|
|
e.preventDefault();
|
2020-05-21 16:46:50 +10:00
|
|
|
onRequestClose();
|
2020-04-13 23:42:18 +10:00
|
|
|
}}
|
2020-05-20 15:44:28 +10:00
|
|
|
sx={{ alignItems: "center" }}
|
2020-04-13 23:42:18 +10:00
|
|
|
>
|
2020-05-20 15:44:28 +10:00
|
|
|
<Text
|
|
|
|
|
as="label"
|
|
|
|
|
variant="body2"
|
|
|
|
|
sx={{ width: "45%", fontSize: "16px" }}
|
|
|
|
|
p={1}
|
|
|
|
|
>
|
|
|
|
|
Label:
|
|
|
|
|
</Text>
|
2020-04-13 23:42:18 +10:00
|
|
|
<Input
|
|
|
|
|
id="changeTokenLabel"
|
|
|
|
|
onChange={handleLabelChange}
|
2020-05-22 13:46:52 +10:00
|
|
|
value={(tokenState && tokenState.label) || ""}
|
2020-04-13 23:42:18 +10:00
|
|
|
sx={{
|
|
|
|
|
padding: "4px",
|
|
|
|
|
border: "none",
|
|
|
|
|
":focus": {
|
|
|
|
|
outline: "none",
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
/>
|
2020-05-20 15:44:28 +10:00
|
|
|
</Flex>
|
2020-04-13 23:42:18 +10:00
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexWrap: "wrap",
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
}}
|
|
|
|
|
>
|
2020-04-20 10:28:40 +10:00
|
|
|
{colorOptions.map((color) => (
|
2020-04-13 23:42:18 +10:00
|
|
|
<Box
|
2020-04-20 10:28:40 +10:00
|
|
|
key={color}
|
2020-04-13 23:42:18 +10:00
|
|
|
sx={{
|
2020-05-20 15:44:28 +10:00
|
|
|
width: "16.66%",
|
|
|
|
|
paddingTop: "16.66%",
|
2020-04-13 23:42:18 +10:00
|
|
|
borderRadius: "50%",
|
|
|
|
|
transform: "scale(0.75)",
|
2020-04-20 10:28:40 +10:00
|
|
|
backgroundColor: colors[color],
|
2020-04-13 23:42:18 +10:00
|
|
|
cursor: "pointer",
|
|
|
|
|
}}
|
2020-04-20 10:28:40 +10:00
|
|
|
onClick={() => handleStatusChange(color)}
|
2020-04-20 23:54:07 +10:00
|
|
|
aria-label={`Token label Color ${color}`}
|
2020-04-13 23:42:18 +10:00
|
|
|
>
|
2020-05-22 13:46:52 +10:00
|
|
|
{tokenState &&
|
|
|
|
|
tokenState.statuses &&
|
|
|
|
|
tokenState.statuses.includes(color) && (
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: "100%",
|
|
|
|
|
border: "2px solid white",
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: 0,
|
|
|
|
|
borderRadius: "50%",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2020-04-13 23:42:18 +10:00
|
|
|
</Box>
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
2020-05-20 15:44:28 +10:00
|
|
|
<Flex sx={{ alignItems: "center" }}>
|
|
|
|
|
<Text
|
|
|
|
|
as="label"
|
|
|
|
|
variant="body2"
|
|
|
|
|
sx={{ width: "40%", fontSize: "16px" }}
|
|
|
|
|
p={1}
|
|
|
|
|
>
|
|
|
|
|
Size:
|
|
|
|
|
</Text>
|
|
|
|
|
<Slider
|
2020-05-22 13:46:52 +10:00
|
|
|
value={(tokenState && tokenState.size) || 1}
|
2020-05-20 15:44:28 +10:00
|
|
|
onChange={handleSizeChange}
|
|
|
|
|
step={1}
|
|
|
|
|
min={1}
|
|
|
|
|
max={tokenMaxSize}
|
|
|
|
|
mr={1}
|
|
|
|
|
/>
|
|
|
|
|
</Flex>
|
2020-04-13 00:24:03 +10:00
|
|
|
</Box>
|
2020-04-20 10:56:51 +10:00
|
|
|
</MapMenu>
|
2020-04-13 00:24:03 +10:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default TokenMenu;
|