Typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-17 14:36:39 +10:00
parent fecf8090ea
commit 97734a2f55
25 changed files with 296 additions and 181 deletions

View File

@ -5,7 +5,7 @@ import { Data } from "@dnd-kit/core/dist/store/types";
type DraggableProps = { type DraggableProps = {
id: string; id: string;
children: React.ReactNode; children: React.ReactNode;
data: Data; data?: Data;
}; };
function Draggable({ id, children, data }: DraggableProps) { function Draggable({ id, children, data }: DraggableProps) {

View File

@ -259,11 +259,9 @@ function Map({
onMapTokenStateRemove(state); onMapTokenStateRemove(state);
setTokenDraggingOptions(undefined); setTokenDraggingOptions(undefined);
}} }}
onTokenStateChange={onMapTokenStateChange}
tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState} tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState}
tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup} tokenNode={tokenDraggingOptions && tokenDraggingOptions.tokenNode}
dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)} dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)}
token={tokensById[tokenDraggingOptions.tokenState.tokenId]}
/> />
); );

View File

@ -89,7 +89,7 @@ function MapTokens({
setTokenDraggingOptions({ setTokenDraggingOptions({
dragging: true, dragging: true,
tokenState, tokenState,
tokenGroup: e.target, tokenNode: e.target,
}) })
} }
onTokenDragEnd={() => onTokenDragEnd={() =>

View File

@ -1,11 +1,18 @@
import React, { useState } from "react"; import { useState } from "react";
import { IconButton } from "theme-ui"; import { IconButton } from "theme-ui";
import SelectTokensIcon from "../../icons/SelectTokensIcon"; import SelectTokensIcon from "../../icons/SelectTokensIcon";
import SelectTokensModal from "../../modals/SelectTokensModal"; import SelectTokensModal from "../../modals/SelectTokensModal";
import { MapTokensStateCreateHandler } from "../../types/Events";
function SelectTokensButton({ onMapTokensStateCreate }) { type SelectTokensButtonProps = {
onMapTokensStateCreate: MapTokensStateCreateHandler;
};
function SelectTokensButton({
onMapTokensStateCreate,
}: SelectTokensButtonProps) {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
function openModal() { function openModal() {
setIsModalOpen(true); setIsModalOpen(true);
@ -14,9 +21,6 @@ function SelectTokensButton({ onMapTokensStateCreate }) {
setIsModalOpen(false); setIsModalOpen(false);
} }
function handleDone() {
closeModal();
}
return ( return (
<> <>
<IconButton <IconButton
@ -29,7 +33,6 @@ function SelectTokensButton({ onMapTokensStateCreate }) {
<SelectTokensModal <SelectTokensModal
isOpen={isModalOpen} isOpen={isModalOpen}
onRequestClose={closeModal} onRequestClose={closeModal}
onDone={handleDone}
onMapTokensStateCreate={onMapTokensStateCreate} onMapTokensStateCreate={onMapTokensStateCreate}
/> />
</> </>

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Box, Flex, Grid } from "theme-ui"; import { Box, Flex, Grid } from "theme-ui";
import SimpleBar from "simplebar-react"; import SimpleBar from "simplebar-react";
@ -9,6 +9,7 @@ import {
KeyboardSensor, KeyboardSensor,
useSensor, useSensor,
useSensors, useSensors,
DragStartEvent,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import TokenBarToken from "./TokenBarToken"; import TokenBarToken from "./TokenBarToken";
@ -23,7 +24,7 @@ import usePreventSelect from "../../hooks/usePreventSelect";
import { useTokenData } from "../../contexts/TokenDataContext"; import { useTokenData } from "../../contexts/TokenDataContext";
import { useUserId } from "../../contexts/UserIdContext"; import { useUserId } from "../../contexts/UserIdContext";
import { useMapStage } from "../../contexts/MapStageContext"; import { useMapStage } from "../../contexts/MapStageContext";
import DragContext from "../../contexts/DragContext"; import DragContext, { CustomDragEndEvent } from "../../contexts/DragContext";
import { import {
createTokenState, createTokenState,
@ -31,13 +32,19 @@ import {
} from "../../helpers/token"; } from "../../helpers/token";
import { findGroup } from "../../helpers/group"; import { findGroup } from "../../helpers/group";
import Vector2 from "../../helpers/Vector2"; import Vector2 from "../../helpers/Vector2";
import { MapTokensStateCreateHandler } from "../../types/Events";
import { Group } from "../../types/Group";
function TokenBar({ onMapTokensStateCreate }) { type TokenBarProps = {
onMapTokensStateCreate: MapTokensStateCreateHandler;
};
function TokenBar({ onMapTokensStateCreate }: TokenBarProps) {
const userId = useUserId(); const userId = useUserId();
const { tokensById, tokenGroups } = useTokenData(); const { tokensById, tokenGroups } = useTokenData();
const [fullScreen] = useSetting("map.fullScreen"); const [fullScreen] = useSetting<boolean>("map.fullScreen");
const [dragId, setDragId] = useState(); const [dragId, setDragId] = useState<string | null>(null);
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
@ -52,14 +59,20 @@ function TokenBar({ onMapTokensStateCreate }) {
const [preventSelect, resumeSelect] = usePreventSelect(); const [preventSelect, resumeSelect] = usePreventSelect();
function handleDragStart({ active }) { function handleDragStart({ active }: DragStartEvent) {
setDragId(active.id); setDragId(active.id);
preventSelect(); preventSelect();
} }
function handleDragEnd({ active, overlayNodeClientRect }) { function handleDragEnd({
active,
overlayNodeClientRect,
}: CustomDragEndEvent) {
setDragId(null); setDragId(null);
resumeSelect();
if (!userId) {
return;
}
const mapStage = mapStageRef.current; const mapStage = mapStageRef.current;
if (mapStage && overlayNodeClientRect) { if (mapStage && overlayNodeClientRect) {
const dragRect = overlayNodeClientRect; const dragRect = overlayNodeClientRect;
@ -96,8 +109,6 @@ function TokenBar({ onMapTokensStateCreate }) {
} }
} }
} }
resumeSelect();
} }
function handleDragCancel() { function handleDragCancel() {
@ -105,7 +116,7 @@ function TokenBar({ onMapTokensStateCreate }) {
resumeSelect(); resumeSelect();
} }
function renderToken(group, draggable = true) { function renderToken(group: Group, draggable = true) {
if (group.type === "item") { if (group.type === "item") {
const token = tokensById[group.id]; const token = tokensById[group.id];
if (token && !token.hideInSidebar) { if (token && !token.hideInSidebar) {
@ -140,6 +151,8 @@ function TokenBar({ onMapTokensStateCreate }) {
} }
} }
const dragGroup = dragId && findGroup(tokenGroups, dragId);
return ( return (
<DragContext <DragContext
onDragStart={handleDragStart} onDragStart={handleDragStart}
@ -188,7 +201,7 @@ function TokenBar({ onMapTokensStateCreate }) {
</Flex> </Flex>
{createPortal( {createPortal(
<DragOverlay dropAnimation={null}> <DragOverlay dropAnimation={null}>
{dragId && renderToken(findGroup(tokenGroups, dragId), false)} {dragGroup && renderToken(dragGroup, false)}
</DragOverlay>, </DragOverlay>,
document.body document.body
)} )}

View File

@ -1,10 +1,15 @@
import React from "react";
import { Box } from "theme-ui"; import { Box } from "theme-ui";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import TokenImage from "./TokenImage"; import TokenImage from "./TokenImage";
function TokenBarToken({ token }) { import { Token } from "../../types/Token";
type TokenBarTokenProps = {
token: Token;
};
function TokenBarToken({ token }: TokenBarTokenProps) {
const [ref, inView] = useInView({ triggerOnce: true }); const [ref, inView] = useInView({ triggerOnce: true });
return ( return (

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from "react"; import { useState, useRef } from "react";
import { Grid, Flex, Box } from "theme-ui"; import { Grid, Flex, Box } from "theme-ui";
import { useSpring, animated } from "react-spring"; import { useSpring, animated } from "react-spring";
import { useDraggable } from "@dnd-kit/core"; import { useDraggable } from "@dnd-kit/core";
@ -11,10 +11,22 @@ import Draggable from "../drag/Draggable";
import Vector2 from "../../helpers/Vector2"; import Vector2 from "../../helpers/Vector2";
import GroupIcon from "../../icons/GroupIcon"; import GroupIcon from "../../icons/GroupIcon";
import { GroupContainer } from "../../types/Group";
import { Token } from "../../types/Token";
function TokenBarTokenGroup({ group, tokens, draggable }) { type TokenBarTokenGroupProps = {
group: GroupContainer;
tokens: Token[];
draggable: boolean;
};
function TokenBarTokenGroup({
group,
tokens,
draggable,
}: TokenBarTokenGroupProps) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: draggable && group.id, id: group.id,
disabled: !draggable, disabled: !draggable,
}); });
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -23,7 +35,7 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
height: isOpen ? (tokens.length + 1) * 56 : 56, height: isOpen ? (tokens.length + 1) * 56 : 56,
}); });
function renderToken(token) { function renderToken(token: Token) {
if (draggable) { if (draggable) {
return ( return (
<Draggable id={token.id} key={token.id}> <Draggable id={token.id} key={token.id}>
@ -77,7 +89,6 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
gridTemplateRows: "1fr 1fr", gridTemplateRows: "1fr 1fr",
}} }}
p="2px" p="2px"
alt={group.name}
title={group.name} title={group.name}
{...listeners} {...listeners}
{...attributes} {...attributes}
@ -100,10 +111,13 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
// Reject the opening of a group if the pointer has moved // Reject the opening of a group if the pointer has moved
const clickDownPositionRef = useRef(new Vector2(0, 0)); const clickDownPositionRef = useRef(new Vector2(0, 0));
function handleOpenDown(event) { function handleOpenDown(event: React.PointerEvent<HTMLDivElement>) {
clickDownPositionRef.current = new Vector2(event.clientX, event.clientY); clickDownPositionRef.current = new Vector2(event.clientX, event.clientY);
} }
function handleOpenClick(event, newOpen) { function handleOpenClick(
event: React.MouseEvent<HTMLDivElement>,
newOpen: boolean
) {
const clickPosition = new Vector2(event.clientX, event.clientY); const clickPosition = new Vector2(event.clientX, event.clientY);
const distance = Vector2.distance( const distance = Vector2.distance(
clickPosition, clickPosition,

View File

@ -1,56 +0,0 @@
import React from "react";
import { useUserId } from "../../contexts/UserIdContext";
import {
useMapWidth,
useMapHeight,
} from "../../contexts/MapInteractionContext";
import DragOverlay from "../map/DragOverlay";
function TokenDragOverlay({
onTokenStateRemove,
onTokenStateChange,
token,
tokenState,
tokenGroup,
dragging,
}) {
const userId = useUserId();
const mapWidth = useMapWidth();
const mapHeight = useMapHeight();
function handleTokenRemove() {
// Handle other tokens when a vehicle gets deleted
if (token && token.category === "vehicle") {
const layer = tokenGroup.getLayer();
const mountedTokens = tokenGroup.find(".token");
for (let mountedToken of mountedTokens) {
// Save and restore token position after moving layer
const position = mountedToken.absolutePosition();
mountedToken.moveTo(layer);
mountedToken.absolutePosition(position);
onTokenStateChange({
[mountedToken.id()]: {
x: mountedToken.x() / mapWidth,
y: mountedToken.y() / mapHeight,
lastModifiedBy: userId,
lastModified: Date.now(),
},
});
}
}
onTokenStateRemove(tokenState);
}
return (
<DragOverlay
dragging={dragging}
onRemove={handleTokenRemove}
node={tokenGroup}
/>
);
}
export default TokenDragOverlay;

View File

@ -0,0 +1,33 @@
import Konva from "konva";
import DragOverlay from "../map/DragOverlay";
import { MapTokenStateRemoveHandler } from "../../types/Events";
import { TokenState } from "../../types/TokenState";
type TokenDragOverlayProps = {
onTokenStateRemove: MapTokenStateRemoveHandler;
tokenState: TokenState;
tokenNode: Konva.Node;
dragging: boolean;
};
function TokenDragOverlay({
onTokenStateRemove,
tokenState,
tokenNode,
dragging,
}: TokenDragOverlayProps) {
function handleTokenRemove() {
onTokenStateRemove(tokenState);
}
return (
<DragOverlay
dragging={dragging}
onRemove={handleTokenRemove}
node={tokenNode}
/>
);
}
export default TokenDragOverlay;

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Flex, Close, IconButton } from "theme-ui"; import { Flex, Close, IconButton } from "theme-ui";
import { groupsFromIds, itemsFromGroups } from "../../helpers/group"; import { groupsFromIds, itemsFromGroups } from "../../helpers/group";
@ -15,10 +15,15 @@ import { useKeyboard } from "../../contexts/KeyboardContext";
import shortcuts from "../../shortcuts"; import shortcuts from "../../shortcuts";
function TokenEditBar({ disabled, onLoad }) { type TokenEditBarProps = {
disabled: boolean;
onLoad: (load: boolean) => void;
};
function TokenEditBar({ disabled, onLoad }: TokenEditBarProps) {
const { tokens, removeTokens, updateTokensHidden } = useTokenData(); const { tokens, removeTokens, updateTokensHidden } = useTokenData();
const { activeGroups, selectedGroupIds, onGroupSelect } = useGroup(); const { activeGroups, selectedGroupIds, onClearSelection } = useGroup();
const [allTokensVisible, setAllTokensVisisble] = useState(false); const [allTokensVisible, setAllTokensVisisble] = useState(false);
@ -40,12 +45,12 @@ function TokenEditBar({ disabled, onLoad }) {
setIsTokensRemoveModalOpen(false); setIsTokensRemoveModalOpen(false);
const selectedTokens = getSelectedTokens(); const selectedTokens = getSelectedTokens();
const selectedTokenIds = selectedTokens.map((token) => token.id); const selectedTokenIds = selectedTokens.map((token) => token.id);
onGroupSelect(); onClearSelection();
await removeTokens(selectedTokenIds); await removeTokens(selectedTokenIds);
onLoad(false); onLoad(false);
} }
async function handleTokensHide(hideInSidebar) { async function handleTokensHide(hideInSidebar: boolean) {
const selectedTokens = getSelectedTokens(); const selectedTokens = getSelectedTokens();
const selectedTokenIds = selectedTokens.map((token) => token.id); const selectedTokenIds = selectedTokens.map((token) => token.id);
// Show loading indicator if hiding more than 10 tokens // Show loading indicator if hiding more than 10 tokens
@ -61,7 +66,7 @@ function TokenEditBar({ disabled, onLoad }) {
/** /**
* Shortcuts * Shortcuts
*/ */
function handleKeyDown(event) { function handleKeyDown(event: KeyboardEvent) {
if (disabled) { if (disabled) {
return; return;
} }
@ -101,7 +106,7 @@ function TokenEditBar({ disabled, onLoad }) {
<Close <Close
title="Clear Selection" title="Clear Selection"
aria-label="Clear Selection" aria-label="Clear Selection"
onClick={() => onGroupSelect()} onClick={() => onClearSelection()}
/> />
<Flex> <Flex>
<IconButton <IconButton

View File

@ -1,10 +1,13 @@
import React from "react";
import { Flex } from "theme-ui"; import { Flex } from "theme-ui";
import TokenShowIcon from "../../icons/TokenShowIcon"; import TokenShowIcon from "../../icons/TokenShowIcon";
import TokenHideIcon from "../../icons/TokenHideIcon"; import TokenHideIcon from "../../icons/TokenHideIcon";
function TokenHiddenBadge({ hidden }) { type TokenHiddenBadgeProps = {
hidden: boolean;
};
function TokenHiddenBadge({ hidden }: TokenHiddenBadgeProps) {
return ( return (
<Flex <Flex
sx={{ sx={{

View File

@ -1,13 +1,18 @@
import React, { useState } from "react"; import { useState } from "react";
import { Image, Box } from "theme-ui"; import { Image, Box, ImageProps } from "theme-ui";
import { useDataURL } from "../../contexts/AssetsContext"; import { useDataURL } from "../../contexts/AssetsContext";
import { tokenSources as defaultTokenSources } from "../../tokens"; import { tokenSources as defaultTokenSources } from "../../tokens";
import { Token } from "../../types/Token";
import { TokenOutlineSVG } from "./TokenOutline"; import { TokenOutlineSVG } from "./TokenOutline";
const TokenImage = React.forwardRef(({ token, ...props }, ref) => { type TokenImageProps = {
token: Token;
} & ImageProps;
function TokenImage({ token, ...props }: TokenImageProps) {
const tokenURL = useDataURL( const tokenURL = useDataURL(
token, token,
defaultTokenSources, defaultTokenSources,
@ -35,12 +40,11 @@ const TokenImage = React.forwardRef(({ token, ...props }, ref) => {
<Image <Image
onLoad={() => setShowOutline(false)} onLoad={() => setShowOutline(false)}
src={tokenURL} src={tokenURL}
ref={ref}
style={showOutline ? { display: "none" } : props.style} style={showOutline ? { display: "none" } : props.style}
{...props} {...props}
/> />
</> </>
); );
}); }
export default TokenImage; export default TokenImage;

View File

@ -1,13 +1,21 @@
import React, { useRef, useEffect, useState } from "react"; import Konva from "konva";
import { useRef, useEffect, useState } from "react";
import { Rect, Text, Group } from "react-konva"; import { Rect, Text, Group } from "react-konva";
import useSetting from "../../hooks/useSetting"; import useSetting from "../../hooks/useSetting";
import { TokenState } from "../../types/TokenState";
const maxTokenSize = 3; const maxTokenSize = 3;
const defaultFontSize = 16; const defaultFontSize = 16;
function TokenLabel({ tokenState, width, height }) { type TokenLabelProps = {
const [labelSize] = useSetting("map.labelSize"); tokenState: TokenState;
width: number;
height: number;
};
function TokenLabel({ tokenState, width, height }: TokenLabelProps) {
const [labelSize] = useSetting<number>("map.labelSize");
const paddingY = const paddingY =
(height / 12 / tokenState.size) * Math.min(tokenState.size, maxTokenSize); (height / 12 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
@ -22,7 +30,7 @@ function TokenLabel({ tokenState, width, height }) {
return; return;
} }
let fontSizes = []; let fontSizes: number[] = [];
for (let size = 20 * labelSize; size >= 6; size--) { for (let size = 20 * labelSize; size >= 6; size--) {
const verticalSize = height / size / tokenState.size; const verticalSize = height / size / tokenState.size;
const tokenSize = Math.min(tokenState.size, maxTokenSize); const tokenSize = Math.min(tokenState.size, maxTokenSize);
@ -30,7 +38,7 @@ function TokenLabel({ tokenState, width, height }) {
fontSizes.push(fontSize); fontSizes.push(fontSize);
} }
function findFontScale() { const findFontScale = () => {
const size = fontSizes.reduce((prev, curr) => { const size = fontSizes.reduce((prev, curr) => {
text.fontSize(curr); text.fontSize(curr);
const textWidth = text.getTextWidth() + paddingX * 2; const textWidth = text.getTextWidth() + paddingX * 2;
@ -42,7 +50,7 @@ function TokenLabel({ tokenState, width, height }) {
}, 1); }, 1);
setFontScale(size / defaultFontSize); setFontScale(size / defaultFontSize);
} };
findFontScale(); findFontScale();
}, [ }, [
@ -68,8 +76,8 @@ function TokenLabel({ tokenState, width, height }) {
} }
}, [tokenState.label, paddingX, width, fontScale]); }, [tokenState.label, paddingX, width, fontScale]);
const textRef = useRef(); const textRef = useRef<Konva.Text>(null);
const textSizerRef = useRef(); const textSizerRef = useRef<Konva.Text>(null);
return ( return (
<Group y={height - (defaultFontSize * fontScale + paddingY) / 2}> <Group y={height - (defaultFontSize * fontScale + paddingY) / 2}>

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Box, Input, Flex, Text, IconButton } from "theme-ui"; import { Box, Input, Flex, Text, IconButton } from "theme-ui";
import Konva from "konva";
import Slider from "../Slider"; import Slider from "../Slider";
@ -16,6 +17,22 @@ import HideIcon from "../../icons/TokenHideIcon";
import { useUserId } from "../../contexts/UserIdContext"; import { useUserId } from "../../contexts/UserIdContext";
import {
RequestCloseEventHandler,
TokenStateChangeEventHandler,
} from "../../types/Events";
import { TokenState } from "../../types/TokenState";
import { Map } from "../../types/Map";
type TokenMenuProps = {
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
tokenState: TokenState;
tokenImage: Konva.Node;
onTokenStateChange: TokenStateChangeEventHandler;
map: Map;
};
const defaultTokenMaxSize = 6; const defaultTokenMaxSize = 6;
function TokenMenu({ function TokenMenu({
isOpen, isOpen,
@ -24,7 +41,7 @@ function TokenMenu({
tokenImage, tokenImage,
onTokenStateChange, onTokenStateChange,
map, map,
}) { }: TokenMenuProps) {
const userId = useUserId(); const userId = useUserId();
const wasOpen = usePrevious(isOpen); const wasOpen = usePrevious(isOpen);
@ -39,22 +56,25 @@ function TokenMenu({
if (tokenImage) { if (tokenImage) {
const imageRect = tokenImage.getClientRect(); const imageRect = tokenImage.getClientRect();
const mapElement = document.querySelector(".map"); const mapElement = document.querySelector(".map");
if (mapElement) {
const mapRect = mapElement.getBoundingClientRect(); const mapRect = mapElement.getBoundingClientRect();
// Center X for the menu which is 156px wide // Center X for the menu which is 156px wide
setMenuLeft(mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2); setMenuLeft(
mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2
);
// Y 12px from the bottom // Y 12px from the bottom
setMenuTop(mapRect.top + imageRect.y + imageRect.height + 12); setMenuTop(mapRect.top + imageRect.y + imageRect.height + 12);
} }
} }
}
}, [isOpen, tokenState, wasOpen, tokenImage]); }, [isOpen, tokenState, wasOpen, tokenImage]);
function handleLabelChange(event) { function handleLabelChange(event: React.ChangeEvent<HTMLInputElement>) {
const label = event.target.value.substring(0, 48); const label = event.target.value.substring(0, 48);
tokenState && onTokenStateChange({ [tokenState.id]: { label: label } }); tokenState && onTokenStateChange({ [tokenState.id]: { label: label } });
} }
function handleStatusChange(status) { function handleStatusChange(status: string) {
if (!tokenState) { if (!tokenState) {
return; return;
} }
@ -69,12 +89,12 @@ function TokenMenu({
}); });
} }
function handleSizeChange(event) { function handleSizeChange(event: React.ChangeEvent<HTMLInputElement>) {
const newSize = parseFloat(event.target.value); const newSize = parseFloat(event.target.value);
tokenState && onTokenStateChange({ [tokenState.id]: { size: newSize } }); tokenState && onTokenStateChange({ [tokenState.id]: { size: newSize } });
} }
function handleRotationChange(event) { function handleRotationChange(event: React.ChangeEvent<HTMLInputElement>) {
const newRotation = parseInt(event.target.value); const newRotation = parseInt(event.target.value);
tokenState && tokenState &&
onTokenStateChange({ onTokenStateChange({
@ -96,16 +116,20 @@ function TokenMenu({
}); });
} }
function handleModalContent(node) { function handleModalContent(node: HTMLElement) {
if (node) { if (node) {
// Focus input // Focus input
const tokenLabelInput = node.querySelector("#changeTokenLabel"); const tokenLabelInput =
node.querySelector<HTMLInputElement>("#changeTokenLabel");
if (tokenLabelInput) {
tokenLabelInput.focus(); tokenLabelInput.focus();
tokenLabelInput.select(); tokenLabelInput.select();
}
// Ensure menu is in bounds // Ensure menu is in bounds
const nodeRect = node.getBoundingClientRect(); const nodeRect = node.getBoundingClientRect();
const mapElement = document.querySelector(".map"); const mapElement = document.querySelector(".map");
if (mapElement) {
const mapRect = mapElement.getBoundingClientRect(); const mapRect = mapElement.getBoundingClientRect();
setMenuLeft((prevLeft) => setMenuLeft((prevLeft) =>
Math.min( Math.min(
@ -118,6 +142,7 @@ function TokenMenu({
); );
} }
} }
}
return ( return (
<MapMenu <MapMenu

View File

@ -1,9 +1,19 @@
import React from "react";
import { Rect, Circle, Line } from "react-konva"; import { Rect, Circle, Line } from "react-konva";
import colors from "../../helpers/colors"; import colors from "../../helpers/colors";
import { Outline } from "../../types/Outline";
export function TokenOutlineSVG({ outline, width, height }) { type TokenOutlineSVGProps = {
outline: Outline;
width: number;
height: number;
};
export function TokenOutlineSVG({
outline,
width,
height,
}: TokenOutlineSVGProps) {
if (outline.type === "rect") { if (outline.type === "rect") {
return ( return (
<svg <svg
@ -55,7 +65,12 @@ export function TokenOutlineSVG({ outline, width, height }) {
} }
} }
function TokenOutline({ outline, hidden }) { type TokenOutlineProps = {
outline: Outline;
hidden: boolean;
};
function TokenOutline({ outline, hidden }: TokenOutlineProps) {
const sharedProps = { const sharedProps = {
fill: colors.black, fill: colors.black,
opacity: hidden ? 0 : 0.8, opacity: hidden ? 0 : 0.8,
@ -84,7 +99,7 @@ function TokenOutline({ outline, hidden }) {
<Line <Line
points={outline.points} points={outline.points}
closed closed
tension={outline.points < 200 ? 0 : 0.33} tension={outline.points.length < 200 ? 0 : 0.33}
{...sharedProps} {...sharedProps}
/> />
); );

View File

@ -1,8 +1,9 @@
import React, { useState, useRef, useEffect } from "react"; import { useState, useRef } from "react";
import { Box, IconButton } from "theme-ui"; import { Box, IconButton } from "theme-ui";
import { Stage, Layer, Image, Rect, Group } from "react-konva"; import { Stage, Layer, Image, Rect, Group } from "react-konva";
import ReactResizeDetector from "react-resize-detector"; import ReactResizeDetector from "react-resize-detector";
import useImage from "use-image"; import useImage from "use-image";
import Konva from "konva";
import usePreventOverscroll from "../../hooks/usePreventOverscroll"; import usePreventOverscroll from "../../hooks/usePreventOverscroll";
import useStageInteraction from "../../hooks/useStageInteraction"; import useStageInteraction from "../../hooks/useStageInteraction";
@ -18,32 +19,32 @@ import GridOffIcon from "../../icons/GridOffIcon";
import { tokenSources } from "../../tokens"; import { tokenSources } from "../../tokens";
import Grid from "../Grid"; import Grid from "../Grid";
import { Token } from "../../types/Token";
function TokenPreview({ token }) { type TokenPreviewProps = {
const [tokenSourceData, setTokenSourceData] = useState({}); token: Token;
useEffect(() => { };
if (token.id !== tokenSourceData.id) {
setTokenSourceData(token);
}
}, [token, tokenSourceData]);
const tokenURL = useDataURL(tokenSourceData, tokenSources); function TokenPreview({ token }: TokenPreviewProps) {
const [tokenSourceImage] = useImage(tokenURL); const tokenURL = useDataURL(token, tokenSources);
const [tokenSourceImage] = useImage(tokenURL || "");
const [stageWidth, setStageWidth] = useState(1); const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1); const [stageHeight, setStageHeight] = useState(1);
const [stageScale, setStageScale] = useState(1); const [stageScale, setStageScale] = useState(1);
const stageTranslateRef = useRef({ x: 0, y: 0 }); const stageTranslateRef = useRef({ x: 0, y: 0 });
const tokenStageRef = useRef(); const tokenStageRef = useRef<Konva.Stage>(null);
const tokenLayerRef = useRef(); const tokenLayerRef = useRef<Konva.Layer>(null);
function handleResize(width, height) { function handleResize(width?: number, height?: number) {
if (width && height) {
setStageWidth(width); setStageWidth(width);
setStageHeight(height); setStageHeight(height);
} }
}
const containerRef = useRef(); const containerRef = useRef<HTMLDivElement>(null);
usePreventOverscroll(containerRef); usePreventOverscroll(containerRef);
const [tokenWidth, tokenHeight] = useImageCenter( const [tokenWidth, tokenHeight] = useImageCenter(
@ -59,11 +60,11 @@ function TokenPreview({ token }) {
); );
useStageInteraction( useStageInteraction(
tokenStageRef.current, tokenStageRef,
stageScale, stageScale,
setStageScale, setStageScale,
stageTranslateRef, stageTranslateRef,
tokenLayerRef.current tokenLayerRef
); );
const [showGridPreview, setShowGridPreview] = useState(true); const [showGridPreview, setShowGridPreview] = useState(true);

View File

@ -1,17 +1,25 @@
import React from "react";
import { Flex, Box, Input, Label } from "theme-ui"; import { Flex, Box, Input, Label } from "theme-ui";
import { isEmpty } from "../../helpers/shared"; import { isEmpty } from "../../helpers/shared";
import Select from "../Select"; import Select from "../Select";
const categorySettings = [ import { Token, TokenCategory } from "../../types/Token";
import { TokenSettingsChangeEventHandler } from "../../types/Events";
type CategorySetting = { value: TokenCategory; label: string };
const categorySettings: CategorySetting[] = [
{ value: "character", label: "Character" }, { value: "character", label: "Character" },
{ value: "prop", label: "Prop" }, { value: "prop", label: "Prop" },
{ value: "vehicle", label: "Vehicle / Mount" }, { value: "vehicle", label: "Vehicle / Mount" },
]; ];
function TokenSettings({ token, onSettingsChange }) { type TokenSettingsProps = {
token: Token;
onSettingsChange: TokenSettingsChangeEventHandler;
};
function TokenSettings({ token, onSettingsChange }: TokenSettingsProps) {
const tokenEmpty = !token || isEmpty(token); const tokenEmpty = !token || isEmpty(token);
return ( return (
<Flex sx={{ flexDirection: "column" }}> <Flex sx={{ flexDirection: "column" }}>
@ -20,7 +28,7 @@ function TokenSettings({ token, onSettingsChange }) {
<Input <Input
name="name" name="name"
value={(token && token.name) || ""} value={(token && token.name) || ""}
onChange={(e) => onSettingsChange("name", e.target.value)} onChange={(e) => onSettingsChange({ name: e.target.value })}
disabled={tokenEmpty} disabled={tokenEmpty}
my={1} my={1}
/> />
@ -30,12 +38,14 @@ function TokenSettings({ token, onSettingsChange }) {
<Select <Select
options={categorySettings} options={categorySettings}
value={ value={
!tokenEmpty && tokenEmpty
categorySettings.find((s) => s.value === token.defaultCategory) ? undefined
: categorySettings.find((s) => s.value === token.defaultCategory)
} }
isDisabled={tokenEmpty} isDisabled={tokenEmpty}
onChange={(option) => onChange={
onSettingsChange("defaultCategory", option.value) ((option: CategorySetting) =>
onSettingsChange({ defaultCategory: option.value })) as any
} }
isSearchable={false} isSearchable={false}
/> />
@ -47,7 +57,7 @@ function TokenSettings({ token, onSettingsChange }) {
name="tokenSize" name="tokenSize"
value={`${(token && token.defaultSize) || 0}`} value={`${(token && token.defaultSize) || 0}`}
onChange={(e) => onChange={(e) =>
onSettingsChange("defaultSize", parseFloat(e.target.value)) onSettingsChange({ defaultSize: parseFloat(e.target.value) })
} }
disabled={tokenEmpty} disabled={tokenEmpty}
min={1} min={1}
@ -59,7 +69,7 @@ function TokenSettings({ token, onSettingsChange }) {
<Input <Input
name="label" name="label"
value={(token && token.defaultLabel) || ""} value={(token && token.defaultLabel) || ""}
onChange={(e) => onSettingsChange("defaultLabel", e.target.value)} onChange={(e) => onSettingsChange({ defaultLabel: e.target.value })}
disabled={tokenEmpty} disabled={tokenEmpty}
my={1} my={1}
/> />

View File

@ -1,9 +1,15 @@
import React from "react";
import { Circle, Group } from "react-konva"; import { Circle, Group } from "react-konva";
import colors from "../../helpers/colors"; import colors from "../../helpers/colors";
import { TokenState } from "../../types/TokenState";
function TokenStatus({ tokenState, width, height }) { type TokenStatusProps = {
tokenState: TokenState;
width: number;
height: number;
};
function TokenStatus({ tokenState, width, height }: TokenStatusProps) {
// Ensure statuses is an array and filter empty values // Ensure statuses is an array and filter empty values
const statuses = [...new Set((tokenState?.statuses || []).filter((s) => s))]; const statuses = [...new Set((tokenState?.statuses || []).filter((s) => s))];
return ( return (

View File

@ -1,8 +1,18 @@
import React from "react"; import React from "react";
import { Token } from "../../types/Token";
import Tile from "../tile/Tile"; import Tile from "../tile/Tile";
import TokenImage from "./TokenImage"; import TokenImage from "./TokenImage";
type TokenTileProps = {
token: Token;
isSelected: boolean;
onSelect: (tokenId: string) => void;
onTokenEdit: (tokenId: string) => void;
canEdit: boolean;
badges: React.ReactChild[];
};
function TokenTile({ function TokenTile({
token, token,
isSelected, isSelected,
@ -10,7 +20,7 @@ function TokenTile({
onTokenEdit, onTokenEdit,
canEdit, canEdit,
badges, badges,
}) { }: TokenTileProps) {
return ( return (
<Tile <Tile
title={token.name} title={token.name}

View File

@ -1,10 +1,19 @@
import React from "react";
import { Grid } from "theme-ui"; import { Grid } from "theme-ui";
import Tile from "../tile/Tile"; import Tile from "../tile/Tile";
import TokenImage from "./TokenImage"; import TokenImage from "./TokenImage";
import useResponsiveLayout from "../../hooks/useResponsiveLayout"; import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import { GroupContainer } from "../../types/Group";
import { Token } from "../../types/Token";
type TokenTileProps = {
group: GroupContainer;
tokens: Token[];
isSelected: boolean;
onSelect: (tokenId: string) => void;
onDoubleClick: () => void;
};
function TokenTileGroup({ function TokenTileGroup({
group, group,
@ -12,7 +21,7 @@ function TokenTileGroup({
isSelected, isSelected,
onSelect, onSelect,
onDoubleClick, onDoubleClick,
}) { }: TokenTileProps) {
const layout = useResponsiveLayout(); const layout = useResponsiveLayout();
return ( return (

View File

@ -1,5 +1,3 @@
import React from "react";
import TokenTile from "./TokenTile"; import TokenTile from "./TokenTile";
import TokenTileGroup from "./TokenTileGroup"; import TokenTileGroup from "./TokenTileGroup";
import TokenHiddenBadge from "./TokenHiddenBadge"; import TokenHiddenBadge from "./TokenHiddenBadge";
@ -10,12 +8,20 @@ import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
import { getGroupItems } from "../../helpers/group"; import { getGroupItems } from "../../helpers/group";
import { useGroup } from "../../contexts/GroupContext"; import { useGroup } from "../../contexts/GroupContext";
import { Token } from "../../types/Token";
import { Group } from "../../types/Group";
function TokenTiles({ tokensById, onTokenEdit, subgroup }) { type TokenTilesProps = {
tokensById: Record<string, Token>;
onTokenEdit: (tokenId: string) => void;
subgroup: boolean;
};
function TokenTiles({ tokensById, onTokenEdit, subgroup }: TokenTilesProps) {
const { selectedGroupIds, selectMode, onGroupOpen, onGroupSelect } = const { selectedGroupIds, selectMode, onGroupOpen, onGroupSelect } =
useGroup(); useGroup();
function renderTile(group) { function renderTile(group: Group) {
if (group.type === "item") { if (group.type === "item") {
const token = tokensById[group.id]; const token = tokensById[group.id];
if (token) { if (token) {

View File

@ -44,10 +44,10 @@ function EditTokenModal({
>({}); >({});
// TODO: CHANGE MAP BACK? OR CHANGE THIS TO PARTIAL // TODO: CHANGE MAP BACK? OR CHANGE THIS TO PARTIAL
function handleTokenSettingsChange(key: string, value: Pick<Token, any>) { function handleTokenSettingsChange(change: Partial<Token>) {
setTokenSettingChanges((prevChanges) => ({ setTokenSettingChanges((prevChanges) => ({
...prevChanges, ...prevChanges,
[key]: value, ...change,
})); }));
} }
@ -67,7 +67,7 @@ function EditTokenModal({
const selectedTokenWithChanges = { const selectedTokenWithChanges = {
...token, ...token,
...tokenSettingChanges, ...tokenSettingChanges,
}; } as Token;
const layout = useResponsiveLayout(); const layout = useResponsiveLayout();

View File

@ -3,6 +3,7 @@ import { DefaultDice } from "./Dice";
import { Map } from "./Map"; import { Map } from "./Map";
import { MapState } from "./MapState"; import { MapState } from "./MapState";
import { Note } from "./Note"; import { Note } from "./Note";
import { Token } from "./Token";
import { TokenState } from "./TokenState"; import { TokenState } from "./TokenState";
export type MapChangeEventHandler = (map?: Map, mapState?: MapState) => void; export type MapChangeEventHandler = (map?: Map, mapState?: MapState) => void;
@ -26,6 +27,7 @@ export type TokenMenuOpenChangeEventHandler = (
tokenStateId: string, tokenStateId: string,
tokenImage: Konva.Node tokenImage: Konva.Node
) => void; ) => void;
export type TokenSettingsChangeEventHandler = (change: Partial<Token>) => void;
export type NoteAddEventHander = (note: Note) => void; export type NoteAddEventHander = (note: Note) => void;
export type NoteRemoveEventHander = (noteId: string) => void; export type NoteRemoveEventHander = (noteId: string) => void;

View File

@ -40,5 +40,5 @@ export type TokenMenuOptions = {
export type TokenDraggingOptions = { export type TokenDraggingOptions = {
dragging: boolean; dragging: boolean;
tokenState: TokenState; tokenState: TokenState;
tokenGroup: Konva.Node; tokenNode: Konva.Node;
}; };

View File

@ -1,3 +1,4 @@
import { Color } from "../helpers/colors";
import { Outline } from "./Outline"; import { Outline } from "./Outline";
import { TokenCategory } from "./Token"; import { TokenCategory } from "./Token";
@ -8,7 +9,7 @@ export type BaseTokenState = {
size: number; size: number;
category: TokenCategory; category: TokenCategory;
label: string; label: string;
statuses: string[]; statuses: Color[];
x: number; x: number;
y: number; y: number;
lastModifiedBy: string; lastModifiedBy: string;