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,82 @@
import React, { useRef, useState, useEffect } from "react";
import { IconButton } from "theme-ui";
import AddMapModal from "../../modals/AddMapModal";
import AddMapIcon from "../../icons/AddMapIcon";
const defaultMapSize = 22;
function AddMapButton({ onMapChange }) {
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
function openModal() {
setIsAddModalOpen(true);
}
function closeModal() {
setIsAddModalOpen(false);
}
const [imageLoaded, setImageLoaded] = useState(false);
const mapDataRef = useRef(null);
const [mapSource, setMapSource] = useState(null);
function handleImageUpload(file, fileGridX, fileGridY) {
const url = URL.createObjectURL(file);
let image = new Image();
image.onload = function () {
mapDataRef.current = {
file,
gridX: fileGridX || gridX,
gridY: fileGridY || gridY,
width: image.width,
height: image.height,
};
setImageLoaded(true);
};
image.src = url;
setMapSource(url);
if (fileGridX) {
setGridX(fileGridX);
}
if (fileGridY) {
setGridY(fileGridY);
}
}
function handleDone() {
if (mapDataRef.current && mapSource) {
onMapChange(mapDataRef.current, mapSource);
}
closeModal();
}
const [gridX, setGridX] = useState(defaultMapSize);
const [gridY, setGridY] = useState(defaultMapSize);
useEffect(() => {
if (mapDataRef.current) {
mapDataRef.current.gridX = gridX;
mapDataRef.current.gridY = gridY;
}
}, [gridX, gridY]);
return (
<>
<IconButton aria-label="Add Map" title="Add Map" onClick={openModal}>
<AddMapIcon />
</IconButton>
<AddMapModal
isOpen={isAddModalOpen}
onRequestClose={closeModal}
onDone={handleDone}
onImageUpload={handleImageUpload}
gridX={gridX}
onGridXChange={setGridX}
gridY={gridY}
onGridYChange={setGridY}
imageLoaded={imageLoaded}
mapSource={mapSource}
/>
</>
);
}
export default AddMapButton;

328
src/components/map/Map.js Normal file
View File

@@ -0,0 +1,328 @@
import React, { useRef, useEffect, useState } from "react";
import { Box, Image } from "theme-ui";
import interact from "interactjs";
import ProxyToken from "../token/ProxyToken";
import TokenMenu from "../token/TokenMenu";
import MapToken from "./MapToken";
import MapDrawing from "./MapDrawing";
import MapControls from "./MapControls";
import { omit } from "../../helpers/shared";
const mapTokenProxyClassName = "map-token__proxy";
const mapTokenMenuClassName = "map-token__menu";
const zoomSpeed = -0.005;
const minZoom = 0.1;
const maxZoom = 5;
function Map({
mapSource,
mapData,
tokens,
onMapTokenChange,
onMapTokenRemove,
onMapChange,
onMapDraw,
onMapDrawUndo,
onMapDrawRedo,
drawActions,
drawActionIndex,
}) {
function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onMapTokenChange) {
onMapTokenChange(token);
}
if (!isOnMap && onMapTokenRemove) {
onMapTokenRemove(token);
}
}
/**
* Map drawing
*/
const [selectedTool, setSelectedTool] = useState("pan");
const [brushColor, setBrushColor] = useState("black");
const [useBrushGridSnapping, setUseBrushGridSnapping] = useState(false);
const [useBrushBlending, setUseBrushBlending] = useState(false);
const [useBrushGesture, setUseBrushGesture] = useState(false);
const [drawnShapes, setDrawnShapes] = useState([]);
function handleShapeAdd(shape) {
onMapDraw({ type: "add", shapes: [shape] });
}
function handleShapeRemove(shapeId) {
onMapDraw({ type: "remove", shapeIds: [shapeId] });
}
function handleShapeRemoveAll() {
onMapDraw({ type: "remove", shapeIds: drawnShapes.map((s) => s.id) });
}
// Replay the draw actions and convert them to shapes for the map drawing
useEffect(() => {
let shapesById = {};
for (let i = 0; i <= drawActionIndex; i++) {
const action = drawActions[i];
if (action.type === "add") {
for (let shape of action.shapes) {
shapesById[shape.id] = shape;
}
}
if (action.type === "remove") {
shapesById = omit(shapesById, action.shapeIds);
}
}
setDrawnShapes(Object.values(shapesById));
}, [drawActions, drawActionIndex]);
const disabledTools = [];
if (!mapData) {
disabledTools.push("pan");
disabledTools.push("brush");
}
if (drawnShapes.length === 0) {
disabledTools.push("erase");
}
/**
* Map movement
*/
const mapTranslateRef = useRef({ x: 0, y: 0 });
const mapScaleRef = useRef(1);
const mapMoveContainerRef = useRef();
function setTranslateAndScale(newTranslate, newScale) {
const moveContainer = mapMoveContainerRef.current;
moveContainer.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px) scale(${newScale})`;
mapScaleRef.current = newScale;
mapTranslateRef.current = newTranslate;
}
useEffect(() => {
function handleMove(event, isGesture) {
const scale = mapScaleRef.current;
const translate = mapTranslateRef.current;
let newScale = scale;
let newTranslate = translate;
if (isGesture) {
newScale = Math.max(Math.min(scale + event.ds, maxZoom), minZoom);
}
if (selectedTool === "pan" || isGesture) {
newTranslate = {
x: translate.x + event.dx,
y: translate.y + event.dy,
};
}
setTranslateAndScale(newTranslate, newScale);
}
const mapInteract = interact(".map")
.gesturable({
listeners: {
move: (e) => handleMove(e, true),
},
})
.draggable({
inertia: true,
listeners: {
move: (e) => handleMove(e, false),
},
cursorChecker: () => {
return selectedTool === "pan" && mapData ? "move" : "default";
},
})
.on("doubletap", (event) => {
event.preventDefault();
if (selectedTool === "pan") {
setTranslateAndScale({ x: 0, y: 0 }, 1);
}
});
return () => {
mapInteract.unset();
};
}, [selectedTool, mapData]);
// Reset map transform when map changes
useEffect(() => {
setTranslateAndScale({ x: 0, y: 0 }, 1);
}, [mapSource]);
// Bind the wheel event of the map via a ref
// in order to support non-passive event listening
// to allow the track pad zoom to be interrupted
// see https://github.com/facebook/react/issues/14856
useEffect(() => {
const mapContainer = mapContainerRef.current;
function handleZoom(event) {
// Stop overscroll on chrome and safari
// also stop pinch to zoom on chrome
event.preventDefault();
const scale = mapScaleRef.current;
const translate = mapTranslateRef.current;
const deltaY = event.deltaY * zoomSpeed;
const newScale = Math.max(Math.min(scale + deltaY, maxZoom), minZoom);
setTranslateAndScale(translate, newScale);
}
if (mapContainer) {
mapContainer.addEventListener("wheel", handleZoom, {
passive: false,
});
}
return () => {
if (mapContainer) {
mapContainer.removeEventListener("wheel", handleZoom);
}
};
}, []);
/**
* Member setup
*/
const mapRef = useRef(null);
const mapContainerRef = useRef();
const gridX = mapData && mapData.gridX;
const gridY = mapData && mapData.gridY;
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
const tokenSizePercent = gridSizeNormalized.x * 100;
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}
className="mapImage"
sx={{
width: "100%",
userSelect: "none",
touchAction: "none",
}}
src={mapSource}
/>
</Box>
);
const mapTokens = (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: "none",
}}
>
{Object.values(tokens).map((token) => (
<MapToken
key={token.id}
token={token}
tokenSizePercent={tokenSizePercent}
className={`${mapTokenProxyClassName} ${mapTokenMenuClassName}`}
/>
))}
</Box>
);
return (
<>
<Box
className="map"
sx={{
flexGrow: 1,
position: "relative",
overflow: "hidden",
backgroundColor: "rgba(0, 0, 0, 0.1)",
userSelect: "none",
touchAction: "none",
}}
bg="background"
ref={mapContainerRef}
>
<Box
sx={{
position: "relative",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
<Box ref={mapMoveContainerRef}>
<Box
sx={{
width: "100%",
height: 0,
paddingBottom: `${(1 / aspectRatio) * 100}%`,
}}
/>
{mapImage}
<MapDrawing
width={mapData ? mapData.width : 0}
height={mapData ? mapData.height : 0}
selectedTool={selectedTool}
shapes={drawnShapes}
onShapeAdd={handleShapeAdd}
onShapeRemove={handleShapeRemove}
brushColor={brushColor}
useGridSnapping={useBrushGridSnapping}
gridSize={gridSizeNormalized}
useBrushBlending={useBrushBlending}
useBrushGesture={useBrushGesture}
/>
{mapTokens}
</Box>
</Box>
<MapControls
onMapChange={onMapChange}
onToolChange={setSelectedTool}
selectedTool={selectedTool}
disabledTools={disabledTools}
onUndo={onMapDrawUndo}
onRedo={onMapDrawRedo}
undoDisabled={drawActionIndex < 0}
redoDisabled={drawActionIndex === drawActions.length - 1}
brushColor={brushColor}
onBrushColorChange={setBrushColor}
onEraseAll={handleShapeRemoveAll}
useBrushGridSnapping={useBrushGridSnapping}
onBrushGridSnappingChange={setUseBrushGridSnapping}
useBrushBlending={useBrushBlending}
onBrushBlendingChange={setUseBrushBlending}
useBrushGesture={useBrushGesture}
onBrushGestureChange={setUseBrushGesture}
/>
</Box>
<ProxyToken
tokenClassName={mapTokenProxyClassName}
onProxyDragEnd={handleProxyDragEnd}
/>
<TokenMenu
tokenClassName={mapTokenMenuClassName}
onTokenChange={onMapTokenChange}
/>
</>
);
}
export default Map;

View File

@@ -0,0 +1,298 @@
import React, { useState, useEffect, useRef } from "react";
import { Flex, Box, IconButton, Label } from "theme-ui";
import AddMapButton from "./AddMapButton";
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
import PanToolIcon from "../../icons/PanToolIcon";
import BrushToolIcon from "../../icons/BrushToolIcon";
import EraseToolIcon from "../../icons/EraseToolIcon";
import UndoIcon from "../../icons/UndoIcon";
import RedoIcon from "../../icons/RedoIcon";
import GridOnIcon from "../../icons/GridOnIcon";
import GridOffIcon from "../../icons/GridOffIcon";
import BlendOnIcon from "../../icons/BlendOnIcon";
import BlendOffIcon from "../../icons/BlendOffIcon";
import GestureOnIcon from "../../icons/GestureOnIcon";
import GestureOffIcon from "../../icons/GestureOffIcon";
import colors, { colorOptions } from "../../helpers/colors";
import MapMenu from "./MapMenu";
import EraseAllIcon from "../../icons/EraseAllIcon";
function MapControls({
onMapChange,
onToolChange,
selectedTool,
disabledTools,
onUndo,
onRedo,
undoDisabled,
redoDisabled,
brushColor,
onBrushColorChange,
onEraseAll,
useBrushGridSnapping,
onBrushGridSnappingChange,
useBrushBlending,
onBrushBlendingChange,
useBrushGesture,
onBrushGestureChange,
}) {
const [isExpanded, setIsExpanded] = useState(false);
const subMenus = {
brush: (
<Box sx={{ width: "104px" }} p={1}>
<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={() => onBrushColorChange(color)}
aria-label={`Brush Color ${color}`}
>
{brushColor === color && (
<Box
sx={{
width: "100%",
height: "100%",
border: "2px solid white",
position: "absolute",
top: 0,
borderRadius: "50%",
}}
/>
)}
</Box>
))}
</Box>
<Flex sx={{ justifyContent: "space-between" }}>
<IconButton
aria-label={
useBrushGridSnapping
? "Disable Brush Grid Snapping"
: "Enable Brush Grid Snapping"
}
title={
useBrushGridSnapping
? "Disable Brush Grid Snapping"
: "Enable Brush Grid Snapping"
}
onClick={() => onBrushGridSnappingChange(!useBrushGridSnapping)}
>
{useBrushGridSnapping ? <GridOnIcon /> : <GridOffIcon />}
</IconButton>
<IconButton
aria-label={
useBrushBlending
? "Disable Brush Blending"
: "Enable Brush Blending"
}
title={
useBrushBlending
? "Disable Brush Blending"
: "Enable Brush Blending"
}
onClick={() => onBrushBlendingChange(!useBrushBlending)}
>
{useBrushBlending ? <BlendOnIcon /> : <BlendOffIcon />}
</IconButton>
<IconButton
aria-label={
useBrushGesture
? "Disable Gesture Detection"
: "Enable Gesture Detection"
}
title={
useBrushGesture
? "Disable Gesture Detection"
: "Enable Gesture Detection"
}
onClick={() => onBrushGestureChange(!useBrushGesture)}
>
{useBrushGesture ? <GestureOnIcon /> : <GestureOffIcon />}
</IconButton>
</Flex>
</Box>
),
erase: (
<Box p={1} pr={3}>
<Label
sx={{
fontSize: 1,
alignItems: "center",
":hover": { color: "primary", cursor: "pointer" },
":active": { color: "secondary" },
}}
>
<IconButton
aria-label="Erase All"
title="Erase All"
onClick={() => {
onEraseAll();
setCurrentSubmenu(null);
setCurrentSubmenuOptions({});
}}
>
<EraseAllIcon />
</IconButton>
Erase All
</Label>
</Box>
),
};
const [currentSubmenu, setCurrentSubmenu] = useState(null);
const [currentSubmenuOptions, setCurrentSubmenuOptions] = useState({});
function handleToolClick(event, tool) {
if (tool !== selectedTool) {
onToolChange(tool);
} else if (currentSubmenu) {
setCurrentSubmenu(null);
setCurrentSubmenuOptions({});
} else if (subMenus[tool]) {
const toolRect = event.target.getBoundingClientRect();
setCurrentSubmenu(tool);
setCurrentSubmenuOptions({
// Align the right of the submenu to the left of the tool and center vertically
left: `${toolRect.left - 16}px`,
top: `${toolRect.bottom - toolRect.height / 2}px`,
style: { transform: "translate(-100%, -50%)" },
// Exclude this node from the sub menus auto close
excludeNode: event.target,
});
}
}
// Detect when a tool becomes disabled and switch to to the pan tool
useEffect(() => {
if (disabledTools.includes(selectedTool)) {
onToolChange("pan");
}
}, [selectedTool, disabledTools, onToolChange]);
const divider = (
<Box
my={2}
bg="text"
sx={{ height: "2px", width: "24px", borderRadius: "2px", opacity: 0.5 }}
></Box>
);
const expanedMenuRef = useRef();
return (
<>
<Flex
sx={{
position: "absolute",
top: 0,
right: 0,
flexDirection: "column",
alignItems: "center",
}}
mx={1}
>
<IconButton
aria-label={isExpanded ? "Hide Map Controls" : "Show Map Controls"}
title={isExpanded ? "Hide Map Controls" : "Show Map Controls"}
onClick={() => setIsExpanded(!isExpanded)}
sx={{
transform: `rotate(${isExpanded ? "0" : "180deg"})`,
display: "block",
backgroundColor: "overlay",
borderRadius: "50%",
}}
m={2}
>
<ExpandMoreIcon />
</IconButton>
<Box
sx={{
flexDirection: "column",
alignItems: "center",
display: isExpanded ? "flex" : "none",
backgroundColor: "overlay",
borderRadius: "4px",
}}
p={2}
ref={expanedMenuRef}
>
<AddMapButton onMapChange={onMapChange} />
{divider}
<IconButton
aria-label="Pan Tool"
title="Pan Tool"
onClick={(e) => handleToolClick(e, "pan")}
sx={{ color: selectedTool === "pan" ? "primary" : "text" }}
disabled={disabledTools.includes("pan")}
>
<PanToolIcon />
</IconButton>
<IconButton
aria-label="Brush Tool"
title="Brush Tool"
onClick={(e) => handleToolClick(e, "brush")}
sx={{ color: selectedTool === "brush" ? "primary" : "text" }}
disabled={disabledTools.includes("brush")}
>
<BrushToolIcon />
</IconButton>
<IconButton
aria-label="Erase Tool"
title="Erase Tool"
onClick={(e) => handleToolClick(e, "erase")}
sx={{ color: selectedTool === "erase" ? "primary" : "text" }}
disabled={disabledTools.includes("erase")}
>
<EraseToolIcon />
</IconButton>
{divider}
<IconButton
aria-label="Undo"
title="Undo"
onClick={() => onUndo()}
disabled={undoDisabled}
>
<UndoIcon />
</IconButton>
<IconButton
aria-label="Redo"
title="Redo"
onClick={() => onRedo()}
disabled={redoDisabled}
>
<RedoIcon />
</IconButton>
</Box>
</Flex>
<MapMenu
isOpen={!!currentSubmenu}
onRequestClose={() => {
setCurrentSubmenu(null);
setCurrentSubmenuOptions({});
}}
{...currentSubmenuOptions}
>
{currentSubmenu && subMenus[currentSubmenu]}
</MapMenu>
</>
);
}
export default MapControls;

View File

@@ -0,0 +1,233 @@
import React, { useRef, useEffect, useState } from "react";
import simplify from "simplify-js";
import shortid from "shortid";
import colors from "../../helpers/colors";
import { snapPositionToGrid } from "../../helpers/shared";
import { pointsToGesture, gestureToData } from "../../helpers/gestures";
function MapDrawing({
width,
height,
selectedTool,
shapes,
onShapeAdd,
onShapeRemove,
brushColor,
useGridSnapping,
gridSize,
useBrushBlending,
useBrushGesture,
}) {
const canvasRef = useRef();
const containerRef = useRef();
const [brushPoints, setBrushPoints] = useState([]);
const [isDrawing, setIsDrawing] = useState(false);
const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
// Reset pointer position when tool changes
useEffect(() => {
setPointerPosition({ x: -1, y: -1 });
}, [selectedTool]);
function getRelativePointerPosition(event) {
const container = containerRef.current;
if (container) {
const containerRect = container.getBoundingClientRect();
const x = (event.clientX - containerRect.x) / containerRect.width;
const y = (event.clientY - containerRect.y) / containerRect.height;
return { x, y };
}
}
function handleStart(event) {
if (event.touches && event.touches.length !== 1) {
setIsDrawing(false);
setBrushPoints([]);
return;
}
const pointer = event.touches ? event.touches[0] : event;
const position = getRelativePointerPosition(pointer);
setPointerPosition(position);
setIsDrawing(true);
if (selectedTool === "brush") {
const brushPosition = useGridSnapping
? snapPositionToGrid(position, gridSize)
: position;
setBrushPoints([brushPosition]);
}
}
function handleMove(event) {
if (event.touches && event.touches.length !== 1) {
return;
}
const pointer = event.touches ? event.touches[0] : event;
const position = getRelativePointerPosition(pointer);
if (selectedTool === "erase") {
setPointerPosition(position);
}
if (isDrawing && selectedTool === "brush") {
setPointerPosition(position);
const brushPosition = useGridSnapping
? snapPositionToGrid(position, gridSize)
: position;
setBrushPoints((prevPoints) => {
if (prevPoints[prevPoints.length - 1] === brushPosition) {
return prevPoints;
}
return [...prevPoints, brushPosition];
});
}
}
function handleStop(event) {
if (event.touches && event.touches.length !== 0) {
return;
}
setIsDrawing(false);
if (selectedTool === "brush") {
if (brushPoints.length > 1) {
const simplifiedPoints = simplify(brushPoints, 0.001);
const type = useBrushGesture
? pointsToGesture(simplifiedPoints)
: "path";
if (type !== null) {
const data =
type === "path"
? { points: simplifiedPoints }
: gestureToData(simplifiedPoints, type);
onShapeAdd({
type,
data,
id: shortid.generate(),
color: brushColor,
blend: useBrushBlending,
});
}
setBrushPoints([]);
}
}
if (selectedTool === "erase" && hoveredShapeRef.current) {
onShapeRemove(hoveredShapeRef.current.id);
}
}
const hoveredShapeRef = useRef(null);
useEffect(() => {
function pointsToPath(points) {
const path = new Path2D();
path.moveTo(points[0].x * width, points[0].y * height);
for (let point of points.slice(1)) {
path.lineTo(point.x * width, point.y * height);
}
path.closePath();
return path;
}
function circleToPath(x, y, radius) {
const path = new Path2D();
const minSide = width < height ? width : height;
path.arc(x * width, y * height, radius * minSide, 0, 2 * Math.PI, true);
return path;
}
function rectangleToPath(x, y, w, h) {
const path = new Path2D();
path.rect(x * width, y * height, w * width, h * height);
return path;
}
function shapeToPath(shape) {
const data = shape.data;
if (shape.type === "path") {
return pointsToPath(data.points);
} else if (shape.type === "circle") {
return circleToPath(data.x, data.y, data.radius);
} else if (shape.type === "rectangle") {
return rectangleToPath(data.x, data.y, data.width, data.height);
} else if (shape.type === "triangle") {
return pointsToPath(data.points);
}
}
function drawPath(path, color, blend, context) {
context.globalAlpha = blend ? 0.5 : 1.0;
context.fillStyle = color;
context.strokeStyle = color;
context.stroke(path);
context.fill(path);
}
const canvas = canvasRef.current;
if (canvas) {
const context = canvas.getContext("2d");
context.clearRect(0, 0, width, height);
let hoveredShape = null;
for (let shape of shapes) {
const path = shapeToPath(shape);
// Detect hover
if (selectedTool === "erase") {
if (
context.isPointInPath(
path,
pointerPosition.x * width,
pointerPosition.y * height
)
) {
hoveredShape = shape;
}
}
drawPath(path, colors[shape.color], shape.blend, context);
}
if (selectedTool === "brush" && brushPoints.length > 0) {
const path = pointsToPath(brushPoints);
drawPath(path, colors[brushColor], useBrushBlending, context);
}
if (hoveredShape) {
const path = shapeToPath(hoveredShape);
drawPath(path, "#BB99FF", true, context);
}
hoveredShapeRef.current = hoveredShape;
}
}, [
shapes,
width,
height,
pointerPosition,
isDrawing,
selectedTool,
brushPoints,
brushColor,
useBrushGesture,
useBrushBlending,
]);
return (
<div
style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }}
ref={containerRef}
onMouseDown={handleStart}
onMouseMove={handleMove}
onMouseUp={handleStop}
onTouchStart={handleStart}
onTouchMove={handleMove}
onTouchEnd={handleStop}
>
<canvas
ref={canvasRef}
width={width}
height={height}
style={{ width: "100%", height: "100%" }}
/>
</div>
);
}
export default MapDrawing;

View File

@@ -0,0 +1,97 @@
import React, { useEffect, useState } from "react";
import Modal from "react-modal";
import { useThemeUI } from "theme-ui";
function MapMenu({
isOpen,
onRequestClose,
onModalContent,
top,
left,
bottom,
right,
children,
style,
// A node to exclude from the pointer event for closing
excludeNode,
}) {
// Save modal node in state to ensure that the pointer listeners
// are removed if the open state changed not from the onRequestClose
// callback
const [modalContentNode, setModalContentNode] = useState(null);
useEffect(() => {
// Close modal if interacting with any other element
function handlePointerDown(event) {
const path = event.composedPath();
if (
!path.includes(modalContentNode) &&
!(excludeNode && path.includes(excludeNode))
) {
onRequestClose();
document.body.removeEventListener("pointerdown", handlePointerDown);
}
}
if (modalContentNode) {
document.body.addEventListener("pointerdown", handlePointerDown);
// Check for wheel event to close modal as well
document.body.addEventListener(
"wheel",
() => {
onRequestClose();
},
{ once: true }
);
}
return () => {
if (modalContentNode) {
document.body.removeEventListener("pointerdown", handlePointerDown);
}
};
}, [modalContentNode, excludeNode, onRequestClose]);
function handleModalContent(node) {
setModalContentNode(node);
onModalContent(node);
}
const { theme } = useThemeUI();
return (
<Modal
isOpen={isOpen}
onRequestClose={onRequestClose}
style={{
overlay: { top: "0", bottom: "initial" },
content: {
backgroundColor: theme.colors.overlay,
top,
left,
right,
bottom,
padding: 0,
borderRadius: "4px",
border: "none",
...style,
},
}}
contentRef={handleModalContent}
>
{children}
</Modal>
);
}
MapMenu.defaultProps = {
onModalContent: () => {},
top: "initial",
left: "initial",
right: "initial",
bottom: "initial",
style: {},
excludeNode: null,
};
export default MapMenu;

View File

@@ -0,0 +1,66 @@
import React, { useRef } from "react";
import { Box, Image } from "theme-ui";
import TokenLabel from "../token/TokenLabel";
import TokenStatus from "../token/TokenStatus";
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", // Set display to flex to fix height being calculated wrong
width: "100%",
flexDirection: "column",
}}
>
<Image
className={className}
sx={{
userSelect: "none",
touchAction: "none",
width: "100%",
}}
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}
data-status={token.status}
ref={imageRef}
/>
{token.status && <TokenStatus statuses={token.status.split(" ")} />}
{token.label && <TokenLabel label={token.label} />}
</Box>
</Box>
</Box>
);
}
export default MapToken;