diff --git a/src/components/map/Map.js b/src/components/map/Map.js
index af2e31f..12abc21 100644
--- a/src/components/map/Map.js
+++ b/src/components/map/Map.js
@@ -9,6 +9,8 @@ import MapDrawing from "./MapDrawing";
import MapControls from "./MapControls";
import { omit } from "../../helpers/shared";
+import useDataSource from "../../helpers/useDataSource";
+import { mapSources as defaultMapSources } from "../../maps";
const mapTokenProxyClassName = "map-token__proxy";
const mapTokenMenuClassName = "map-token__menu";
@@ -27,6 +29,8 @@ function Map({
onMapDrawUndo,
onMapDrawRedo,
}) {
+ const mapSource = useDataSource(map, defaultMapSources);
+
function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onMapTokenChange) {
onMapTokenChange(token);
@@ -219,7 +223,7 @@ function Map({
userSelect: "none",
touchAction: "none",
}}
- src={map && map.source}
+ src={mapSource}
/>
);
@@ -323,10 +327,12 @@ function Map({
>
);
diff --git a/src/components/map/MapTile.js b/src/components/map/MapTile.js
index e6f2104..4ef6b41 100644
--- a/src/components/map/MapTile.js
+++ b/src/components/map/MapTile.js
@@ -7,6 +7,9 @@ import RemoveMapIcon from "../../icons/RemoveMapIcon";
import ResetMapIcon from "../../icons/ResetMapIcon";
import ExpandMoreDotIcon from "../../icons/ExpandMoreDotIcon";
+import useDataSource from "../../helpers/useDataSource";
+import { mapSources as defaultMapSources } from "../../maps";
+
function MapTile({
map,
isSelected,
@@ -15,8 +18,10 @@ function MapTile({
onMapReset,
onSubmit,
}) {
+ const mapSource = useDataSource(map, defaultMapSources);
const [isMapTileMenuOpen, setIsTileMenuOpen] = useState(false);
const [hasMapState, setHasMapState] = useState(false);
+ const isDefault = map.type === "default";
useEffect(() => {
async function checkForMapState() {
@@ -28,7 +33,6 @@ function MapTile({
setHasMapState(true);
}
}
-
checkForMapState();
}, [map]);
@@ -120,18 +124,18 @@ function MapTile({
>
{/* Show expand button only if both reset and remove is available */}
{isSelected && (
- {map.default && hasMapState && resetButton(map)}
- {!map.default && hasMapState && !isMapTileMenuOpen && expandButton}
- {!map.default && !hasMapState && removeButton(map)}
+ {isDefault && hasMapState && resetButton(map)}
+ {!isDefault && hasMapState && !isMapTileMenuOpen && expandButton}
+ {!isDefault && !hasMapState && removeButton(map)}
)}
{/* Tile menu for two actions */}
- {!map.default && isMapTileMenuOpen && isSelected && (
+ {!isDefault && isMapTileMenuOpen && isSelected && (
setIsTileMenuOpen(false)}
>
- {!map.default && removeButton(map)}
+ {!isDefault && removeButton(map)}
{hasMapState && resetButton(map)}
)}
diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js
index 5dfb799..3fdceb0 100644
--- a/src/components/map/MapToken.js
+++ b/src/components/map/MapToken.js
@@ -5,8 +5,13 @@ import TokenLabel from "../token/TokenLabel";
import TokenStatus from "../token/TokenStatus";
import usePreventTouch from "../../helpers/usePreventTouch";
+import useDataSource from "../../helpers/useDataSource";
+
+import { tokenSources } from "../../tokens";
function MapToken({ token, tokenSizePercent, className }) {
+ const imageSource = useDataSource(token, tokenSources);
+
const imageRef = useRef();
// Stop touch to prevent 3d touch gesutre on iOS
usePreventTouch(imageRef);
@@ -47,15 +52,12 @@ function MapToken({ token, tokenSizePercent, className }) {
touchAction: "none",
width: "100%",
}}
- src={token.image}
- // pass data into the dom element used to pass state to the ProxyToken
+ src={imageSource}
+ // pass id into the dom element which is then used by the ProxyToken
data-id={token.id}
- data-size={token.size}
- data-label={token.label}
- data-status={token.status}
ref={imageRef}
/>
- {token.status && }
+ {token.statuses && }
{token.label && }
diff --git a/src/components/token/ListToken.js b/src/components/token/ListToken.js
index ad83430..bbdb3a2 100644
--- a/src/components/token/ListToken.js
+++ b/src/components/token/ListToken.js
@@ -1,20 +1,29 @@
import React, { useRef } from "react";
-import { Image } from "theme-ui";
+import { Box, Image } from "theme-ui";
import usePreventTouch from "../../helpers/usePreventTouch";
+import useDataSource from "../../helpers/useDataSource";
+
+import { tokenSources } from "../../tokens";
+
+function ListToken({ token, className }) {
+ const imageSource = useDataSource(token, tokenSources);
-function ListToken({ image, className }) {
const imageRef = useRef();
// Stop touch to prevent 3d touch gesutre on iOS
usePreventTouch(imageRef);
return (
-
+
+
+
);
}
diff --git a/src/components/token/ProxyToken.js b/src/components/token/ProxyToken.js
index 6a17c84..08e26c7 100644
--- a/src/components/token/ProxyToken.js
+++ b/src/components/token/ProxyToken.js
@@ -8,14 +8,32 @@ import usePortal from "../../helpers/usePortal";
import TokenLabel from "./TokenLabel";
import TokenStatus from "./TokenStatus";
-function ProxyToken({ tokenClassName, onProxyDragEnd }) {
+/**
+ * @callback onProxyDragEnd
+ * @param {boolean} isOnMap whether the token was dropped on the map
+ * @param {Object} token the token that was dropped
+ */
+
+/**
+ *
+ * @param {string} tokenClassName The class name to attach the interactjs handler to
+ * @param {onProxyDragEnd} onProxyDragEnd Called when the proxy token is dropped
+ * @param {Object} tokens An optional mapping of tokens to use as a base when calling OnProxyDragEnd
+ */
+function ProxyToken({ tokenClassName, onProxyDragEnd, tokens }) {
const proxyContainer = usePortal("root");
const [imageSource, setImageSource] = useState("");
- const [label, setLabel] = useState("");
- const [status, setStatus] = useState("");
+ const [tokenId, setTokenId] = useState(null);
const proxyRef = useRef();
+ // Store the tokens in a ref and access in the interactjs loop
+ // This is needed to stop interactjs from creating multiple listeners
+ const tokensRef = useRef(tokens);
+ useEffect(() => {
+ tokensRef.current = tokens;
+ }, [tokens]);
+
const proxyOnMap = useRef(false);
useEffect(() => {
@@ -26,8 +44,7 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
// 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 || "");
+ setTokenId(target.dataset.id);
let proxy = proxyRef.current;
if (proxy) {
@@ -88,13 +105,14 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
x = x / (mapImageRect.right - mapImageRect.left);
y = y / (mapImageRect.bottom - mapImageRect.top);
- target.setAttribute("data-x", x);
- target.setAttribute("data-y", y);
+ // Get the token from the supplied tokens if it exists
+ const id = target.getAttribute("data-id");
+ const token = tokensRef.current[id] || {};
onProxyDragEnd(proxyOnMap.current, {
- image: target.src,
- // Pass in props stored as data- in the dom node
- ...target.dataset,
+ ...token,
+ x,
+ y,
});
}
@@ -140,12 +158,20 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
width: "100%",
}}
/>
- {status && }
- {label && }
+ {tokens[tokenId] && tokens[tokenId].statuses && (
+
+ )}
+ {tokens[tokenId] && tokens[tokenId].label && (
+
+ )}
,
proxyContainer
);
}
+ProxyToken.defaultProps = {
+ tokens: {},
+};
+
export default ProxyToken;
diff --git a/src/components/token/TokenMenu.js b/src/components/token/TokenMenu.js
index 90b555a..74c4eb2 100644
--- a/src/components/token/TokenMenu.js
+++ b/src/components/token/TokenMenu.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useState, useRef } from "react";
import interact from "interactjs";
import { Box, Input } from "theme-ui";
@@ -6,13 +6,31 @@ import MapMenu from "../map/MapMenu";
import colors, { colorOptions } from "../../helpers/colors";
-function TokenMenu({ tokenClassName, onTokenChange }) {
+/**
+ * @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
+ */
+function TokenMenu({ tokenClassName, onTokenChange, tokens }) {
const [isOpen, setIsOpen] = useState(false);
function handleRequestClose() {
setIsOpen(false);
}
+ // Store the tokens in a ref and access in the interactjs loop
+ // This is needed to stop interactjs from creating multiple listeners
+ const tokensRef = useRef(tokens);
+ useEffect(() => {
+ tokensRef.current = tokens;
+ }, [tokens]);
+
const [currentToken, setCurrentToken] = useState({});
const [menuLeft, setMenuLeft] = useState(0);
const [menuTop, setMenuTop] = useState(0);
@@ -31,30 +49,26 @@ function TokenMenu({ tokenClassName, onTokenChange }) {
}
function handleStatusChange(status) {
- const statuses =
- currentToken.status.split(" ").filter((s) => s !== "") || [];
+ const statuses = currentToken.statuses;
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,
+ statuses: newStatuses,
}));
- onTokenChange({ ...currentToken, status: newStatus });
+ onTokenChange({ ...currentToken, statuses: newStatuses });
}
useEffect(() => {
function handleTokenMenuOpen(event) {
const target = event.target;
- const dataset = (target && target.dataset) || {};
- setCurrentToken({
- image: target.src,
- ...dataset,
- });
+ const id = target.getAttribute("data-id");
+ const token = tokensRef.current[id] || {};
+ setCurrentToken(token);
const targetRect = target.getBoundingClientRect();
setMenuLeft(targetRect.left);
@@ -162,7 +176,7 @@ function TokenMenu({ tokenClassName, onTokenChange }) {
onClick={() => handleStatusChange(color)}
aria-label={`Token label Color ${color}`}
>
- {currentToken.status && currentToken.status.includes(color) && (
+ {currentToken.statuses && currentToken.statuses.includes(color) && (
{
+ const defaultTokensWithIds = [];
+ for (let defaultToken of defaultTokens) {
+ defaultTokensWithIds.push({ ...defaultToken, id: defaultToken.name });
+ }
+ setTokens(defaultTokensWithIds);
+ }, []);
+
const [tokenSize, setTokenSize] = useState(1);
function handleProxyDragEnd(isOnMap, token) {
@@ -22,7 +33,7 @@ function Tokens({ onCreateMapToken }) {
id: shortid.generate(),
size: tokenSize,
label: "",
- status: "",
+ statuses: [],
});
}
}
@@ -38,10 +49,12 @@ function Tokens({ onCreateMapToken }) {
}}
>
- {Object.entries(tokens).map(([id, image]) => (
-
-
-
+ {tokens.map((token) => (
+
))}
@@ -57,6 +70,7 @@ function Tokens({ onCreateMapToken }) {
[token.id, token]))}
/>
>
);
diff --git a/src/helpers/useDataSource.js b/src/helpers/useDataSource.js
new file mode 100644
index 0000000..a57f69d
--- /dev/null
+++ b/src/helpers/useDataSource.js
@@ -0,0 +1,29 @@
+import { useEffect, useState } from "react";
+
+// Helper function to load either file or default data
+// into a URL and ensure that it is revoked if needed
+function useDataSource(data, defaultSources) {
+ const [dataSource, setDataSource] = useState(null);
+ useEffect(() => {
+ if (!data) {
+ return;
+ }
+ let url = null;
+ if (data.type === "file") {
+ url = URL.createObjectURL(data.file);
+ } else if (data.type === "default") {
+ url = defaultSources[data.name];
+ }
+ setDataSource(url);
+
+ return () => {
+ if (data.type === "file" && url) {
+ URL.revokeObjectURL(url);
+ }
+ };
+ }, [data, defaultSources]);
+
+ return dataSource;
+}
+
+export default useDataSource;
diff --git a/src/maps/index.js b/src/maps/index.js
index 1c0eb07..0b55b37 100644
--- a/src/maps/index.js
+++ b/src/maps/index.js
@@ -5,46 +5,20 @@ import stoneImage from "./Stone Grid 22x22.jpg";
import waterImage from "./Water Grid 22x22.jpg";
import woodImage from "./Wood Grid 22x22.jpg";
-const defaultProps = {
+export const mapSources = {
+ blank: blankImage,
+ grass: grassImage,
+ sand: sandImage,
+ stone: stoneImage,
+ water: waterImage,
+ wood: woodImage,
+};
+
+export const maps = Object.keys(mapSources).map((name) => ({
+ name,
gridX: 22,
gridY: 22,
width: 1024,
height: 1024,
- default: true,
-};
-
-export const blank = {
- ...defaultProps,
- source: blankImage,
- id: "__default_blank",
-};
-
-export const grass = {
- ...defaultProps,
- source: grassImage,
- id: "__default_grass",
-};
-
-export const sand = {
- ...defaultProps,
- source: sandImage,
- id: "__default_sand",
-};
-
-export const stone = {
- ...defaultProps,
- source: stoneImage,
- id: "__default_stone",
-};
-
-export const water = {
- ...defaultProps,
- source: waterImage,
- id: "__default_water",
-};
-
-export const wood = {
- ...defaultProps,
- source: woodImage,
- id: "__default_wood",
-};
+ type: "default",
+}));
diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js
index 2718151..757d4f3 100644
--- a/src/modals/SelectMapModal.js
+++ b/src/modals/SelectMapModal.js
@@ -7,7 +7,7 @@ import db from "../database";
import Modal from "../components/Modal";
import MapTiles from "../components/map/MapTiles";
-import * as defaultMaps from "../maps";
+import { maps as defaultMaps } from "../maps";
const defaultMapSize = 22;
const defaultMapState = {
@@ -37,15 +37,15 @@ function SelectMapModal({
async function loadDefaultMaps() {
const defaultMapsWithIds = [];
const defaultMapStates = [];
- // Store the default maps into the db in reverse so the whie map is first
- // in the UI
- const defaultMapArray = Object.values(defaultMaps).reverse();
- for (let i = 0; i < defaultMapArray.length; i++) {
- const defaultMap = defaultMapArray[i];
- const id = `${defaultMap.id}--${shortid.generate()}`;
+ // Reverse maps to ensure the blank map is first in the list
+ const sortedMaps = [...defaultMaps].reverse();
+ for (let i = 0; i < sortedMaps.length; i++) {
+ const defaultMap = sortedMaps[i];
+ const id = `__default_${defaultMap.name}--${shortid.generate()}`;
defaultMapsWithIds.push({
...defaultMap,
id,
+ // Emulate the time increasing to avoid sort errors
timestamp: Date.now() + i,
});
defaultMapStates.push({ ...defaultMapState, mapId: id });
@@ -64,12 +64,6 @@ function SelectMapModal({
} else {
// Sort maps by the time they were added
storedMaps.sort((a, b) => b.timestamp - a.timestamp);
- for (let map of storedMaps) {
- // Recreate image urls for file based maps
- if (map.file) {
- map.source = URL.createObjectURL(map.file);
- }
- }
setMaps(storedMaps);
}
}
@@ -101,21 +95,23 @@ function SelectMapModal({
}
}
}
- const url = URL.createObjectURL(file);
let image = new Image();
setImageLoading(true);
+ // Create and load the image temporarily to get its dimensions
+ const url = URL.createObjectURL(file);
image.onload = function () {
handleMapAdd({
file,
+ type: "file",
gridX: fileGridX,
gridY: fileGridY,
width: image.width,
height: image.height,
- source: url,
id: shortid.generate(),
timestamp: Date.now(),
});
setImageLoading(false);
+ URL.revokeObjectURL(url);
};
image.src = url;
}
@@ -135,7 +131,6 @@ function SelectMapModal({
setGridY(map.gridY);
}
- // Keep track of removed maps
async function handleMapRemove(id) {
await db.table("maps").delete(id);
await db.table("states").delete(id);
@@ -145,7 +140,7 @@ function SelectMapModal({
return filtered;
});
// Removed the map from the map screen if needed
- if (currentMap.id === selectedMap.id) {
+ if (currentMap && currentMap.id === selectedMap.id) {
onMapChange(null);
}
}
@@ -160,7 +155,7 @@ function SelectMapModal({
const state = { ...defaultMapState, mapId: id };
await db.table("states").put(state);
// Reset the state of the current map if needed
- if (currentMap.id === selectedMap.id) {
+ if (currentMap && currentMap.id === selectedMap.id) {
onMapStateChange(state);
}
}
diff --git a/src/routes/Game.js b/src/routes/Game.js
index 9621d63..4b512e8 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -63,6 +63,9 @@ function Game() {
}
async function handleMapTokenChange(token) {
+ if (mapState === null) {
+ return;
+ }
setMapState((prevMapState) => ({
...prevMapState,
tokens: {
diff --git a/src/tokens/index.js b/src/tokens/index.js
index a6fa5ea..da44f26 100644
--- a/src/tokens/index.js
+++ b/src/tokens/index.js
@@ -19,7 +19,7 @@ import swords from "./Swords.png";
import tree from "./Tree.png";
import triangle from "./Triangle.png";
-export {
+export const tokenSources = {
axes,
bird,
book,
@@ -39,5 +39,10 @@ export {
sun,
swords,
tree,
- triangle
+ triangle,
};
+
+export const tokens = Object.keys(tokenSources).map((name) => ({
+ name,
+ type: "default",
+}));