Refactored component folder structure to be a little clearer

This commit is contained in:
Mitchell McCaffrey
2020-04-23 10:09:12 +10:00
parent 65c3620732
commit f2a92f2ccd
19 changed files with 42 additions and 42 deletions

View 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;

View File

@@ -0,0 +1,151 @@
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 "../../helpers/usePortal";
import TokenLabel from "./TokenLabel";
import TokenStatus from "./TokenStatus";
function ProxyToken({ tokenClassName, onProxyDragEnd }) {
const proxyContainer = usePortal("root");
const [imageSource, setImageSource] = useState("");
const [label, setLabel] = useState("");
const [status, setStatus] = useState("");
const proxyRef = useRef();
const proxyOnMap = useRef(false);
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);
setLabel(target.dataset.label || "");
setStatus(target.dataset.status || "");
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;
let proxy = proxyRef.current;
if (proxy) {
if (onProxyDragEnd) {
const mapImage = document.querySelector(".mapImage");
const mapImageRect = mapImage.getBoundingClientRect();
let x = parseFloat(proxy.getAttribute("data-x")) || 0;
let y = parseFloat(proxy.getAttribute("data-y")) || 0;
// Convert coordiantes to be relative to the map
x = x - mapImageRect.left;
y = y - mapImageRect.top;
// Normalize to map width
x = x / (mapImageRect.right - mapImageRect.left);
y = y / (mapImageRect.bottom - mapImageRect.top);
target.setAttribute("data-x", x);
target.setAttribute("data-y", y);
onProxyDragEnd(proxyOnMap.current, {
image: target.src,
// Pass in props stored as data- in the dom node
...target.dataset,
});
}
// 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]);
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%",
}}
/>
{status && <TokenStatus statuses={status.split(" ")} />}
{label && <TokenLabel label={label} />}
</Box>
</Box>,
proxyContainer
);
}
export default ProxyToken;

View File

@@ -0,0 +1,51 @@
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",
width: "100%",
display: "flex", // Set display to flex to fix height being calculated wrong
flexDirection: "column",
}}
>
<Image sx={{ width: "100%" }} 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;

View File

@@ -0,0 +1,185 @@
import React, { useEffect, useState } from "react";
import interact from "interactjs";
import { Box, Input } from "theme-ui";
import MapMenu from "../map/MapMenu";
import colors, { colorOptions } from "../../helpers/colors";
function TokenMenu({ tokenClassName, onTokenChange }) {
const [isOpen, setIsOpen] = useState(false);
function handleRequestClose() {
setIsOpen(false);
}
const [currentToken, setCurrentToken] = useState({});
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 });
}
}
function handleStatusChange(status) {
const statuses =
currentToken.status.split(" ").filter((s) => s !== "") || [];
let newStatuses = [];
if (statuses.includes(status)) {
newStatuses = statuses.filter((s) => s !== status);
} else {
newStatuses = [...statuses, status];
}
const newStatus = newStatuses.join(" ");
setCurrentToken((prevToken) => ({
...prevToken,
status: newStatus,
}));
onTokenChange({ ...currentToken, status: newStatus });
}
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 tap gesture
const tokenInteract = interact(`.${tokenClassName}`).on(
"tap",
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);
tokenInteract.unset();
};
}, [tokenClassName]);
function handleModalContent(node) {
if (node) {
// Focus input
const tokenLabelInput = node.querySelector("#changeTokenLabel");
tokenLabelInput.focus();
tokenLabelInput.setSelectionRange(7, 8);
// 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)
);
}
}
return (
<MapMenu
isOpen={isOpen}
onRequestClose={handleRequestClose}
top={`${menuTop}px`}
left={`${menuLeft}px`}
onModalContent={handleModalContent}
>
<Box sx={{ width: "104px" }} p={1}>
<Box
as="form"
onSubmit={(e) => {
e.preventDefault();
handleRequestClose();
}}
>
<Input
id="changeTokenLabel"
onChange={handleLabelChange}
value={`Label: ${currentToken.label}`}
sx={{
padding: "4px",
border: "none",
":focus": {
outline: "none",
},
}}
autoComplete="off"
/>
</Box>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
}}
>
{colorOptions.map((color) => (
<Box
key={color}
sx={{
width: "25%",
paddingTop: "25%",
borderRadius: "50%",
transform: "scale(0.75)",
backgroundColor: colors[color],
cursor: "pointer",
}}
onClick={() => handleStatusChange(color)}
aria-label={`Token label Color ${color}`}
>
{currentToken.status && currentToken.status.includes(color) && (
<Box
sx={{
width: "100%",
height: "100%",
border: "2px solid white",
position: "absolute",
top: 0,
borderRadius: "50%",
}}
/>
)}
</Box>
))}
</Box>
</Box>
</MapMenu>
);
}
export default TokenMenu;

View File

@@ -0,0 +1,47 @@
import React from "react";
import { Box } from "theme-ui";
import colors from "../../helpers/colors";
function TokenStatus({ statuses }) {
return (
<Box
sx={{
position: "absolute",
width: "100%",
height: "100%",
pointerEvents: "none",
}}
>
{statuses.map((status, index) => (
<Box
key={status}
sx={{
width: "100%",
height: "100%",
position: "absolute",
opacity: 0.8,
transform: `scale(${1 - index / 10})`,
}}
>
<svg
style={{ position: "absolute" }}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
>
<circle
r={47}
cx={50}
cy={50}
fill="none"
stroke={colors[status]}
strokeWidth={4}
/>
</svg>
</Box>
))}
</Box>
);
}
export default TokenStatus;

View File

@@ -0,0 +1,65 @@
import React, { useState } from "react";
import { Box } from "theme-ui";
import shortid from "shortid";
import SimpleBar from "simplebar-react";
import * as tokens from "../../tokens";
import ListToken from "./ListToken";
import ProxyToken from "./ProxyToken";
import NumberInput from "../NumberInput";
const listTokenClassName = "list-token";
function Tokens({ onCreateMapToken }) {
const [tokenSize, setTokenSize] = useState(1);
function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onCreateMapToken) {
// Give the token an id
onCreateMapToken({
...token,
id: shortid.generate(),
size: tokenSize,
label: "",
status: "",
});
}
}
return (
<>
<Box
sx={{
height: "100%",
width: "80px",
minWidth: "80px",
overflow: "hidden",
}}
>
<SimpleBar style={{ height: "calc(100% - 58px)", overflowX: "hidden" }}>
{Object.entries(tokens).map(([id, image]) => (
<Box key={id} my={2} mx={3} sx={{ width: "48px", height: "48px" }}>
<ListToken image={image} className={listTokenClassName} />
</Box>
))}
</SimpleBar>
<Box pt={1} bg="muted" sx={{ height: "58px" }}>
<NumberInput
value={tokenSize}
onChange={setTokenSize}
title="Size"
min={1}
max={9}
/>
</Box>
</Box>
<ProxyToken
tokenClassName={listTokenClassName}
onProxyDragEnd={handleProxyDragEnd}
/>
</>
);
}
export default Tokens;