diff --git a/src/components/ListToken.js b/src/components/ListToken.js new file mode 100644 index 0000000..f90d3d0 --- /dev/null +++ b/src/components/ListToken.js @@ -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 ( + + ); +} + +export default ListToken; diff --git a/src/components/Map.js b/src/components/Map.js index dc3ec02..6c9b811 100644 --- a/src/components/Map.js +++ b/src/components/Map.js @@ -2,9 +2,10 @@ import React, { useRef, useEffect, useState } from "react"; import { Box, Image } from "theme-ui"; import interact from "interactjs"; -import Token from "../components/Token"; import ProxyToken from "../components/ProxyToken"; import AddMapButton from "../components/AddMapButton"; +import TokenMenu from "../components/TokenMenu"; +import MapToken from "../components/MapToken"; const mapTokenClassName = "map-token"; const zoomSpeed = -0.005; @@ -15,13 +16,13 @@ function Map({ mapSource, mapData, tokens, - onMapTokenMove, + onMapTokenChange, onMapTokenRemove, - onMapChanged, + onMapChange, }) { function handleProxyDragEnd(isOnMap, token) { - if (isOnMap && onMapTokenMove) { - onMapTokenMove(token); + if (isOnMap && onMapTokenChange) { + onMapTokenChange(token); } if (!isOnMap && onMapTokenRemove) { @@ -120,6 +121,64 @@ function Map({ const tokenSizePercent = (1 / rows) * 100; const aspectRatio = (mapData && mapData.width / mapData.height) || 1; + const mapImage = ( + + + + ); + + const mapTokens = ( + + {Object.values(tokens).map((token) => ( + + ))} + + ); + + const mapActions = ( + + + + ); + return ( <> - - - - - {Object.values(tokens).map((token) => ( - - - - ))} - + {mapImage} + {mapTokens} - - - + {mapActions} + ); } diff --git a/src/components/MapToken.js b/src/components/MapToken.js new file mode 100644 index 0000000..fff70cb --- /dev/null +++ b/src/components/MapToken.js @@ -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 ( + + + + + {token.label && } + + + + ); +} + +export default MapToken; diff --git a/src/components/SizeInput.js b/src/components/NumberInput.js similarity index 70% rename from src/components/SizeInput.js rename to src/components/NumberInput.js index 799eefa..e6985f8 100644 --- a/src/components/SizeInput.js +++ b/src/components/NumberInput.js @@ -1,17 +1,17 @@ import React from "react"; import { Box, Flex, IconButton, Text } from "theme-ui"; -function SizeInput({ value, onChange }) { +function NumberInput({ value, onChange, title, min, max }) { return ( - Size + {title} value > 1 && onChange(value - 1)} + aria-label={`Decrease ${title}`} + title={`Decrease ${title}`} + onClick={() => value > min && onChange(value - 1)} > - + {value} onChange(value + 1)} + aria-label={`Increase ${title}`} + title={`Increase ${title}`} + onClick={() => value < max && onChange(value + 1)} > {}, + title: "Number", + min: 0, + max: 10, }; -export default SizeInput; +export default NumberInput; diff --git a/src/components/ProxyToken.js b/src/components/ProxyToken.js index ae38900..27722ad 100644 --- a/src/components/ProxyToken.js +++ b/src/components/ProxyToken.js @@ -86,10 +86,11 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) { x = x / (mapRect.right - mapRect.left); y = y / (mapRect.bottom - mapRect.top); + target.setAttribute("data-x", x); + target.setAttribute("data-y", y); + onProxyDragEnd(proxyOnMap.current, { image: imageSource, - x, - y, // Pass in props stored as data- in the dom node ...target.dataset, }); diff --git a/src/components/Token.js b/src/components/Token.js deleted file mode 100644 index 4c2a191..0000000 --- a/src/components/Token.js +++ /dev/null @@ -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 ( - - ); -} - -Token.defaultProps = { - data: {}, - sx: {}, -}; - -export default Token; diff --git a/src/components/TokenLabel.js b/src/components/TokenLabel.js new file mode 100644 index 0000000..061b853 --- /dev/null +++ b/src/components/TokenLabel.js @@ -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 ( + + + + + + {label} + + + + + ); +} + +export default TokenLabel; diff --git a/src/components/TokenMenu.js b/src/components/TokenMenu.js new file mode 100644 index 0000000..085ea3a --- /dev/null +++ b/src/components/TokenMenu.js @@ -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 ( + + { + e.preventDefault(); + handleRequestClose(); + }} + sx={{ width: "72px" }} + > + + + + ); +} + +export default TokenMenu; diff --git a/src/components/Tokens.js b/src/components/Tokens.js index 6980da5..9882c23 100644 --- a/src/components/Tokens.js +++ b/src/components/Tokens.js @@ -5,9 +5,9 @@ import SimpleBar from "simplebar-react"; import * as tokens from "../tokens"; -import Token from "./Token"; +import ListToken from "./ListToken"; import ProxyToken from "./ProxyToken"; -import SizeInput from "./SizeInput"; +import NumberInput from "./NumberInput"; const listTokenClassName = "list-token"; @@ -17,7 +17,12 @@ function Tokens({ onCreateMapToken }) { function handleProxyDragEnd(isOnMap, token) { if (isOnMap && onCreateMapToken) { // 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 }) { {Object.entries(tokens).map(([id, image]) => ( - + ))} - + { + // 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; diff --git a/src/images/TokenLabel.png b/src/images/TokenLabel.png new file mode 100644 index 0000000..637ddab Binary files /dev/null and b/src/images/TokenLabel.png differ diff --git a/src/routes/Game.js b/src/routes/Game.js index 8d4522c..ddc1e01 100644 --- a/src/routes/Game.js +++ b/src/routes/Game.js @@ -28,7 +28,7 @@ function Game() { const [mapSource, setMapSource] = useState(null); const mapDataRef = useRef(null); - function handleMapChanged(mapData, mapSource) { + function handleMapChange(mapData, mapSource) { mapDataRef.current = mapData; setMapSource(mapSource); for (let peer of Object.values(peers)) { @@ -38,7 +38,7 @@ function Game() { const [mapTokens, setMapTokens] = useState({}); - function handleEditMapToken(token) { + function handleMapTokenChange(token) { if (!mapSource) { return; } @@ -52,7 +52,7 @@ function Game() { } } - function handleRemoveMapToken(token) { + function handleMapTokenRemove(token) { setMapTokens((prevMapTokens) => { const { [token.id]: old, ...rest } = prevMapTokens; return rest; @@ -213,11 +213,11 @@ function Game() { mapSource={mapSource} mapData={mapDataRef.current} tokens={mapTokens} - onMapTokenMove={handleEditMapToken} - onMapTokenRemove={handleRemoveMapToken} - onMapChanged={handleMapChanged} + onMapTokenChange={handleMapTokenChange} + onMapTokenRemove={handleMapTokenRemove} + onMapChange={handleMapChange} /> - + setPeerError(null)}>