Typescript
This commit is contained in:
parent
fecf8090ea
commit
97734a2f55
@ -5,7 +5,7 @@ import { Data } from "@dnd-kit/core/dist/store/types";
|
||||
type DraggableProps = {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
data: Data;
|
||||
data?: Data;
|
||||
};
|
||||
|
||||
function Draggable({ id, children, data }: DraggableProps) {
|
||||
|
@ -259,11 +259,9 @@ function Map({
|
||||
onMapTokenStateRemove(state);
|
||||
setTokenDraggingOptions(undefined);
|
||||
}}
|
||||
onTokenStateChange={onMapTokenStateChange}
|
||||
tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState}
|
||||
tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup}
|
||||
tokenNode={tokenDraggingOptions && tokenDraggingOptions.tokenNode}
|
||||
dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)}
|
||||
token={tokensById[tokenDraggingOptions.tokenState.tokenId]}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -89,7 +89,7 @@ function MapTokens({
|
||||
setTokenDraggingOptions({
|
||||
dragging: true,
|
||||
tokenState,
|
||||
tokenGroup: e.target,
|
||||
tokenNode: e.target,
|
||||
})
|
||||
}
|
||||
onTokenDragEnd={() =>
|
||||
|
@ -1,11 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import SelectTokensIcon from "../../icons/SelectTokensIcon";
|
||||
|
||||
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);
|
||||
function openModal() {
|
||||
setIsModalOpen(true);
|
||||
@ -14,9 +21,6 @@ function SelectTokensButton({ onMapTokensStateCreate }) {
|
||||
setIsModalOpen(false);
|
||||
}
|
||||
|
||||
function handleDone() {
|
||||
closeModal();
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
@ -29,7 +33,6 @@ function SelectTokensButton({ onMapTokensStateCreate }) {
|
||||
<SelectTokensModal
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={closeModal}
|
||||
onDone={handleDone}
|
||||
onMapTokensStateCreate={onMapTokensStateCreate}
|
||||
/>
|
||||
</>
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Box, Flex, Grid } from "theme-ui";
|
||||
import SimpleBar from "simplebar-react";
|
||||
@ -9,6 +9,7 @@ import {
|
||||
KeyboardSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragStartEvent,
|
||||
} from "@dnd-kit/core";
|
||||
|
||||
import TokenBarToken from "./TokenBarToken";
|
||||
@ -23,7 +24,7 @@ import usePreventSelect from "../../hooks/usePreventSelect";
|
||||
import { useTokenData } from "../../contexts/TokenDataContext";
|
||||
import { useUserId } from "../../contexts/UserIdContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
import DragContext from "../../contexts/DragContext";
|
||||
import DragContext, { CustomDragEndEvent } from "../../contexts/DragContext";
|
||||
|
||||
import {
|
||||
createTokenState,
|
||||
@ -31,13 +32,19 @@ import {
|
||||
} from "../../helpers/token";
|
||||
import { findGroup } from "../../helpers/group";
|
||||
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 { 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();
|
||||
|
||||
@ -52,14 +59,20 @@ function TokenBar({ onMapTokensStateCreate }) {
|
||||
|
||||
const [preventSelect, resumeSelect] = usePreventSelect();
|
||||
|
||||
function handleDragStart({ active }) {
|
||||
function handleDragStart({ active }: DragStartEvent) {
|
||||
setDragId(active.id);
|
||||
preventSelect();
|
||||
}
|
||||
|
||||
function handleDragEnd({ active, overlayNodeClientRect }) {
|
||||
function handleDragEnd({
|
||||
active,
|
||||
overlayNodeClientRect,
|
||||
}: CustomDragEndEvent) {
|
||||
setDragId(null);
|
||||
|
||||
resumeSelect();
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const mapStage = mapStageRef.current;
|
||||
if (mapStage && overlayNodeClientRect) {
|
||||
const dragRect = overlayNodeClientRect;
|
||||
@ -96,8 +109,6 @@ function TokenBar({ onMapTokensStateCreate }) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resumeSelect();
|
||||
}
|
||||
|
||||
function handleDragCancel() {
|
||||
@ -105,7 +116,7 @@ function TokenBar({ onMapTokensStateCreate }) {
|
||||
resumeSelect();
|
||||
}
|
||||
|
||||
function renderToken(group, draggable = true) {
|
||||
function renderToken(group: Group, draggable = true) {
|
||||
if (group.type === "item") {
|
||||
const token = tokensById[group.id];
|
||||
if (token && !token.hideInSidebar) {
|
||||
@ -140,6 +151,8 @@ function TokenBar({ onMapTokensStateCreate }) {
|
||||
}
|
||||
}
|
||||
|
||||
const dragGroup = dragId && findGroup(tokenGroups, dragId);
|
||||
|
||||
return (
|
||||
<DragContext
|
||||
onDragStart={handleDragStart}
|
||||
@ -188,7 +201,7 @@ function TokenBar({ onMapTokensStateCreate }) {
|
||||
</Flex>
|
||||
{createPortal(
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{dragId && renderToken(findGroup(tokenGroups, dragId), false)}
|
||||
{dragGroup && renderToken(dragGroup, false)}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
@ -1,10 +1,15 @@
|
||||
import React from "react";
|
||||
import { Box } from "theme-ui";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
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 });
|
||||
|
||||
return (
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { Grid, Flex, Box } from "theme-ui";
|
||||
import { useSpring, animated } from "react-spring";
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
@ -11,10 +11,22 @@ import Draggable from "../drag/Draggable";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
||||
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({
|
||||
id: draggable && group.id,
|
||||
id: group.id,
|
||||
disabled: !draggable,
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@ -23,7 +35,7 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
|
||||
height: isOpen ? (tokens.length + 1) * 56 : 56,
|
||||
});
|
||||
|
||||
function renderToken(token) {
|
||||
function renderToken(token: Token) {
|
||||
if (draggable) {
|
||||
return (
|
||||
<Draggable id={token.id} key={token.id}>
|
||||
@ -77,7 +89,6 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
|
||||
gridTemplateRows: "1fr 1fr",
|
||||
}}
|
||||
p="2px"
|
||||
alt={group.name}
|
||||
title={group.name}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
@ -100,10 +111,13 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
|
||||
|
||||
// Reject the opening of a group if the pointer has moved
|
||||
const clickDownPositionRef = useRef(new Vector2(0, 0));
|
||||
function handleOpenDown(event) {
|
||||
function handleOpenDown(event: React.PointerEvent<HTMLDivElement>) {
|
||||
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 distance = Vector2.distance(
|
||||
clickPosition,
|
@ -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;
|
33
src/components/token/TokenDragOverlay.tsx
Normal file
33
src/components/token/TokenDragOverlay.tsx
Normal 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;
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Flex, Close, IconButton } from "theme-ui";
|
||||
|
||||
import { groupsFromIds, itemsFromGroups } from "../../helpers/group";
|
||||
@ -15,10 +15,15 @@ import { useKeyboard } from "../../contexts/KeyboardContext";
|
||||
|
||||
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 { activeGroups, selectedGroupIds, onGroupSelect } = useGroup();
|
||||
const { activeGroups, selectedGroupIds, onClearSelection } = useGroup();
|
||||
|
||||
const [allTokensVisible, setAllTokensVisisble] = useState(false);
|
||||
|
||||
@ -40,12 +45,12 @@ function TokenEditBar({ disabled, onLoad }) {
|
||||
setIsTokensRemoveModalOpen(false);
|
||||
const selectedTokens = getSelectedTokens();
|
||||
const selectedTokenIds = selectedTokens.map((token) => token.id);
|
||||
onGroupSelect();
|
||||
onClearSelection();
|
||||
await removeTokens(selectedTokenIds);
|
||||
onLoad(false);
|
||||
}
|
||||
|
||||
async function handleTokensHide(hideInSidebar) {
|
||||
async function handleTokensHide(hideInSidebar: boolean) {
|
||||
const selectedTokens = getSelectedTokens();
|
||||
const selectedTokenIds = selectedTokens.map((token) => token.id);
|
||||
// Show loading indicator if hiding more than 10 tokens
|
||||
@ -61,7 +66,7 @@ function TokenEditBar({ disabled, onLoad }) {
|
||||
/**
|
||||
* Shortcuts
|
||||
*/
|
||||
function handleKeyDown(event) {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
@ -101,7 +106,7 @@ function TokenEditBar({ disabled, onLoad }) {
|
||||
<Close
|
||||
title="Clear Selection"
|
||||
aria-label="Clear Selection"
|
||||
onClick={() => onGroupSelect()}
|
||||
onClick={() => onClearSelection()}
|
||||
/>
|
||||
<Flex>
|
||||
<IconButton
|
@ -1,10 +1,13 @@
|
||||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
|
||||
import TokenShowIcon from "../../icons/TokenShowIcon";
|
||||
import TokenHideIcon from "../../icons/TokenHideIcon";
|
||||
|
||||
function TokenHiddenBadge({ hidden }) {
|
||||
type TokenHiddenBadgeProps = {
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
function TokenHiddenBadge({ hidden }: TokenHiddenBadgeProps) {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
@ -1,13 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import { Image, Box } from "theme-ui";
|
||||
import { useState } from "react";
|
||||
import { Image, Box, ImageProps } from "theme-ui";
|
||||
|
||||
import { useDataURL } from "../../contexts/AssetsContext";
|
||||
|
||||
import { tokenSources as defaultTokenSources } from "../../tokens";
|
||||
import { Token } from "../../types/Token";
|
||||
|
||||
import { TokenOutlineSVG } from "./TokenOutline";
|
||||
|
||||
const TokenImage = React.forwardRef(({ token, ...props }, ref) => {
|
||||
type TokenImageProps = {
|
||||
token: Token;
|
||||
} & ImageProps;
|
||||
|
||||
function TokenImage({ token, ...props }: TokenImageProps) {
|
||||
const tokenURL = useDataURL(
|
||||
token,
|
||||
defaultTokenSources,
|
||||
@ -35,12 +40,11 @@ const TokenImage = React.forwardRef(({ token, ...props }, ref) => {
|
||||
<Image
|
||||
onLoad={() => setShowOutline(false)}
|
||||
src={tokenURL}
|
||||
ref={ref}
|
||||
style={showOutline ? { display: "none" } : props.style}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default TokenImage;
|
@ -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 useSetting from "../../hooks/useSetting";
|
||||
import { TokenState } from "../../types/TokenState";
|
||||
|
||||
const maxTokenSize = 3;
|
||||
const defaultFontSize = 16;
|
||||
|
||||
function TokenLabel({ tokenState, width, height }) {
|
||||
const [labelSize] = useSetting("map.labelSize");
|
||||
type TokenLabelProps = {
|
||||
tokenState: TokenState;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
function TokenLabel({ tokenState, width, height }: TokenLabelProps) {
|
||||
const [labelSize] = useSetting<number>("map.labelSize");
|
||||
|
||||
const paddingY =
|
||||
(height / 12 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
|
||||
@ -22,7 +30,7 @@ function TokenLabel({ tokenState, width, height }) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fontSizes = [];
|
||||
let fontSizes: number[] = [];
|
||||
for (let size = 20 * labelSize; size >= 6; size--) {
|
||||
const verticalSize = height / size / tokenState.size;
|
||||
const tokenSize = Math.min(tokenState.size, maxTokenSize);
|
||||
@ -30,7 +38,7 @@ function TokenLabel({ tokenState, width, height }) {
|
||||
fontSizes.push(fontSize);
|
||||
}
|
||||
|
||||
function findFontScale() {
|
||||
const findFontScale = () => {
|
||||
const size = fontSizes.reduce((prev, curr) => {
|
||||
text.fontSize(curr);
|
||||
const textWidth = text.getTextWidth() + paddingX * 2;
|
||||
@ -42,7 +50,7 @@ function TokenLabel({ tokenState, width, height }) {
|
||||
}, 1);
|
||||
|
||||
setFontScale(size / defaultFontSize);
|
||||
}
|
||||
};
|
||||
|
||||
findFontScale();
|
||||
}, [
|
||||
@ -68,8 +76,8 @@ function TokenLabel({ tokenState, width, height }) {
|
||||
}
|
||||
}, [tokenState.label, paddingX, width, fontScale]);
|
||||
|
||||
const textRef = useRef();
|
||||
const textSizerRef = useRef();
|
||||
const textRef = useRef<Konva.Text>(null);
|
||||
const textSizerRef = useRef<Konva.Text>(null);
|
||||
|
||||
return (
|
||||
<Group y={height - (defaultFontSize * fontScale + paddingY) / 2}>
|
@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Box, Input, Flex, Text, IconButton } from "theme-ui";
|
||||
import Konva from "konva";
|
||||
|
||||
import Slider from "../Slider";
|
||||
|
||||
@ -16,6 +17,22 @@ import HideIcon from "../../icons/TokenHideIcon";
|
||||
|
||||
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;
|
||||
function TokenMenu({
|
||||
isOpen,
|
||||
@ -24,7 +41,7 @@ function TokenMenu({
|
||||
tokenImage,
|
||||
onTokenStateChange,
|
||||
map,
|
||||
}) {
|
||||
}: TokenMenuProps) {
|
||||
const userId = useUserId();
|
||||
|
||||
const wasOpen = usePrevious(isOpen);
|
||||
@ -39,22 +56,25 @@ function TokenMenu({
|
||||
if (tokenImage) {
|
||||
const imageRect = tokenImage.getClientRect();
|
||||
const mapElement = document.querySelector(".map");
|
||||
const mapRect = mapElement.getBoundingClientRect();
|
||||
|
||||
// Center X for the menu which is 156px wide
|
||||
setMenuLeft(mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2);
|
||||
// Y 12px from the bottom
|
||||
setMenuTop(mapRect.top + imageRect.y + imageRect.height + 12);
|
||||
if (mapElement) {
|
||||
const mapRect = mapElement.getBoundingClientRect();
|
||||
// Center X for the menu which is 156px wide
|
||||
setMenuLeft(
|
||||
mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2
|
||||
);
|
||||
// Y 12px from the bottom
|
||||
setMenuTop(mapRect.top + imageRect.y + imageRect.height + 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isOpen, tokenState, wasOpen, tokenImage]);
|
||||
|
||||
function handleLabelChange(event) {
|
||||
function handleLabelChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const label = event.target.value.substring(0, 48);
|
||||
tokenState && onTokenStateChange({ [tokenState.id]: { label: label } });
|
||||
}
|
||||
|
||||
function handleStatusChange(status) {
|
||||
function handleStatusChange(status: string) {
|
||||
if (!tokenState) {
|
||||
return;
|
||||
}
|
||||
@ -69,12 +89,12 @@ function TokenMenu({
|
||||
});
|
||||
}
|
||||
|
||||
function handleSizeChange(event) {
|
||||
function handleSizeChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const newSize = parseFloat(event.target.value);
|
||||
tokenState && onTokenStateChange({ [tokenState.id]: { size: newSize } });
|
||||
}
|
||||
|
||||
function handleRotationChange(event) {
|
||||
function handleRotationChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const newRotation = parseInt(event.target.value);
|
||||
tokenState &&
|
||||
onTokenStateChange({
|
||||
@ -96,26 +116,31 @@ function TokenMenu({
|
||||
});
|
||||
}
|
||||
|
||||
function handleModalContent(node) {
|
||||
function handleModalContent(node: HTMLElement) {
|
||||
if (node) {
|
||||
// Focus input
|
||||
const tokenLabelInput = node.querySelector("#changeTokenLabel");
|
||||
tokenLabelInput.focus();
|
||||
tokenLabelInput.select();
|
||||
const tokenLabelInput =
|
||||
node.querySelector<HTMLInputElement>("#changeTokenLabel");
|
||||
if (tokenLabelInput) {
|
||||
tokenLabelInput.focus();
|
||||
tokenLabelInput.select();
|
||||
}
|
||||
|
||||
// Ensure menu is in bounds
|
||||
const nodeRect = node.getBoundingClientRect();
|
||||
const mapElement = document.querySelector(".map");
|
||||
const mapRect = mapElement.getBoundingClientRect();
|
||||
setMenuLeft((prevLeft) =>
|
||||
Math.min(
|
||||
mapRect.right - nodeRect.width,
|
||||
Math.max(mapRect.left, prevLeft)
|
||||
)
|
||||
);
|
||||
setMenuTop((prevTop) =>
|
||||
Math.min(mapRect.bottom - nodeRect.height, prevTop)
|
||||
);
|
||||
if (mapElement) {
|
||||
const mapRect = mapElement.getBoundingClientRect();
|
||||
setMenuLeft((prevLeft) =>
|
||||
Math.min(
|
||||
mapRect.right - nodeRect.width,
|
||||
Math.max(mapRect.left, prevLeft)
|
||||
)
|
||||
);
|
||||
setMenuTop((prevTop) =>
|
||||
Math.min(mapRect.bottom - nodeRect.height, prevTop)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,19 @@
|
||||
import React from "react";
|
||||
import { Rect, Circle, Line } from "react-konva";
|
||||
|
||||
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") {
|
||||
return (
|
||||
<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 = {
|
||||
fill: colors.black,
|
||||
opacity: hidden ? 0 : 0.8,
|
||||
@ -84,7 +99,7 @@ function TokenOutline({ outline, hidden }) {
|
||||
<Line
|
||||
points={outline.points}
|
||||
closed
|
||||
tension={outline.points < 200 ? 0 : 0.33}
|
||||
tension={outline.points.length < 200 ? 0 : 0.33}
|
||||
{...sharedProps}
|
||||
/>
|
||||
);
|
@ -1,8 +1,9 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { Box, IconButton } from "theme-ui";
|
||||
import { Stage, Layer, Image, Rect, Group } from "react-konva";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
import useImage from "use-image";
|
||||
import Konva from "konva";
|
||||
|
||||
import usePreventOverscroll from "../../hooks/usePreventOverscroll";
|
||||
import useStageInteraction from "../../hooks/useStageInteraction";
|
||||
@ -18,32 +19,32 @@ import GridOffIcon from "../../icons/GridOffIcon";
|
||||
import { tokenSources } from "../../tokens";
|
||||
|
||||
import Grid from "../Grid";
|
||||
import { Token } from "../../types/Token";
|
||||
|
||||
function TokenPreview({ token }) {
|
||||
const [tokenSourceData, setTokenSourceData] = useState({});
|
||||
useEffect(() => {
|
||||
if (token.id !== tokenSourceData.id) {
|
||||
setTokenSourceData(token);
|
||||
}
|
||||
}, [token, tokenSourceData]);
|
||||
type TokenPreviewProps = {
|
||||
token: Token;
|
||||
};
|
||||
|
||||
const tokenURL = useDataURL(tokenSourceData, tokenSources);
|
||||
const [tokenSourceImage] = useImage(tokenURL);
|
||||
function TokenPreview({ token }: TokenPreviewProps) {
|
||||
const tokenURL = useDataURL(token, tokenSources);
|
||||
const [tokenSourceImage] = useImage(tokenURL || "");
|
||||
|
||||
const [stageWidth, setStageWidth] = useState(1);
|
||||
const [stageHeight, setStageHeight] = useState(1);
|
||||
const [stageScale, setStageScale] = useState(1);
|
||||
|
||||
const stageTranslateRef = useRef({ x: 0, y: 0 });
|
||||
const tokenStageRef = useRef();
|
||||
const tokenLayerRef = useRef();
|
||||
const tokenStageRef = useRef<Konva.Stage>(null);
|
||||
const tokenLayerRef = useRef<Konva.Layer>(null);
|
||||
|
||||
function handleResize(width, height) {
|
||||
setStageWidth(width);
|
||||
setStageHeight(height);
|
||||
function handleResize(width?: number, height?: number) {
|
||||
if (width && height) {
|
||||
setStageWidth(width);
|
||||
setStageHeight(height);
|
||||
}
|
||||
}
|
||||
|
||||
const containerRef = useRef();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
usePreventOverscroll(containerRef);
|
||||
|
||||
const [tokenWidth, tokenHeight] = useImageCenter(
|
||||
@ -59,11 +60,11 @@ function TokenPreview({ token }) {
|
||||
);
|
||||
|
||||
useStageInteraction(
|
||||
tokenStageRef.current,
|
||||
tokenStageRef,
|
||||
stageScale,
|
||||
setStageScale,
|
||||
stageTranslateRef,
|
||||
tokenLayerRef.current
|
||||
tokenLayerRef
|
||||
);
|
||||
|
||||
const [showGridPreview, setShowGridPreview] = useState(true);
|
@ -1,17 +1,25 @@
|
||||
import React from "react";
|
||||
import { Flex, Box, Input, Label } from "theme-ui";
|
||||
|
||||
import { isEmpty } from "../../helpers/shared";
|
||||
|
||||
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: "prop", label: "Prop" },
|
||||
{ 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);
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
@ -20,7 +28,7 @@ function TokenSettings({ token, onSettingsChange }) {
|
||||
<Input
|
||||
name="name"
|
||||
value={(token && token.name) || ""}
|
||||
onChange={(e) => onSettingsChange("name", e.target.value)}
|
||||
onChange={(e) => onSettingsChange({ name: e.target.value })}
|
||||
disabled={tokenEmpty}
|
||||
my={1}
|
||||
/>
|
||||
@ -30,12 +38,14 @@ function TokenSettings({ token, onSettingsChange }) {
|
||||
<Select
|
||||
options={categorySettings}
|
||||
value={
|
||||
!tokenEmpty &&
|
||||
categorySettings.find((s) => s.value === token.defaultCategory)
|
||||
tokenEmpty
|
||||
? undefined
|
||||
: categorySettings.find((s) => s.value === token.defaultCategory)
|
||||
}
|
||||
isDisabled={tokenEmpty}
|
||||
onChange={(option) =>
|
||||
onSettingsChange("defaultCategory", option.value)
|
||||
onChange={
|
||||
((option: CategorySetting) =>
|
||||
onSettingsChange({ defaultCategory: option.value })) as any
|
||||
}
|
||||
isSearchable={false}
|
||||
/>
|
||||
@ -47,7 +57,7 @@ function TokenSettings({ token, onSettingsChange }) {
|
||||
name="tokenSize"
|
||||
value={`${(token && token.defaultSize) || 0}`}
|
||||
onChange={(e) =>
|
||||
onSettingsChange("defaultSize", parseFloat(e.target.value))
|
||||
onSettingsChange({ defaultSize: parseFloat(e.target.value) })
|
||||
}
|
||||
disabled={tokenEmpty}
|
||||
min={1}
|
||||
@ -59,7 +69,7 @@ function TokenSettings({ token, onSettingsChange }) {
|
||||
<Input
|
||||
name="label"
|
||||
value={(token && token.defaultLabel) || ""}
|
||||
onChange={(e) => onSettingsChange("defaultLabel", e.target.value)}
|
||||
onChange={(e) => onSettingsChange({ defaultLabel: e.target.value })}
|
||||
disabled={tokenEmpty}
|
||||
my={1}
|
||||
/>
|
@ -1,9 +1,15 @@
|
||||
import React from "react";
|
||||
import { Circle, Group } from "react-konva";
|
||||
|
||||
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
|
||||
const statuses = [...new Set((tokenState?.statuses || []).filter((s) => s))];
|
||||
return (
|
@ -1,8 +1,18 @@
|
||||
import React from "react";
|
||||
import { Token } from "../../types/Token";
|
||||
|
||||
import Tile from "../tile/Tile";
|
||||
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({
|
||||
token,
|
||||
isSelected,
|
||||
@ -10,7 +20,7 @@ function TokenTile({
|
||||
onTokenEdit,
|
||||
canEdit,
|
||||
badges,
|
||||
}) {
|
||||
}: TokenTileProps) {
|
||||
return (
|
||||
<Tile
|
||||
title={token.name}
|
@ -1,10 +1,19 @@
|
||||
import React from "react";
|
||||
import { Grid } from "theme-ui";
|
||||
|
||||
import Tile from "../tile/Tile";
|
||||
import TokenImage from "./TokenImage";
|
||||
|
||||
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({
|
||||
group,
|
||||
@ -12,7 +21,7 @@ function TokenTileGroup({
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDoubleClick,
|
||||
}) {
|
||||
}: TokenTileProps) {
|
||||
const layout = useResponsiveLayout();
|
||||
|
||||
return (
|
@ -1,5 +1,3 @@
|
||||
import React from "react";
|
||||
|
||||
import TokenTile from "./TokenTile";
|
||||
import TokenTileGroup from "./TokenTileGroup";
|
||||
import TokenHiddenBadge from "./TokenHiddenBadge";
|
||||
@ -10,12 +8,20 @@ import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
|
||||
import { getGroupItems } from "../../helpers/group";
|
||||
|
||||
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 } =
|
||||
useGroup();
|
||||
|
||||
function renderTile(group) {
|
||||
function renderTile(group: Group) {
|
||||
if (group.type === "item") {
|
||||
const token = tokensById[group.id];
|
||||
if (token) {
|
@ -44,10 +44,10 @@ function EditTokenModal({
|
||||
>({});
|
||||
|
||||
// TODO: CHANGE MAP BACK? OR CHANGE THIS TO PARTIAL
|
||||
function handleTokenSettingsChange(key: string, value: Pick<Token, any>) {
|
||||
function handleTokenSettingsChange(change: Partial<Token>) {
|
||||
setTokenSettingChanges((prevChanges) => ({
|
||||
...prevChanges,
|
||||
[key]: value,
|
||||
...change,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ function EditTokenModal({
|
||||
const selectedTokenWithChanges = {
|
||||
...token,
|
||||
...tokenSettingChanges,
|
||||
};
|
||||
} as Token;
|
||||
|
||||
const layout = useResponsiveLayout();
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { DefaultDice } from "./Dice";
|
||||
import { Map } from "./Map";
|
||||
import { MapState } from "./MapState";
|
||||
import { Note } from "./Note";
|
||||
import { Token } from "./Token";
|
||||
import { TokenState } from "./TokenState";
|
||||
|
||||
export type MapChangeEventHandler = (map?: Map, mapState?: MapState) => void;
|
||||
@ -26,6 +27,7 @@ export type TokenMenuOpenChangeEventHandler = (
|
||||
tokenStateId: string,
|
||||
tokenImage: Konva.Node
|
||||
) => void;
|
||||
export type TokenSettingsChangeEventHandler = (change: Partial<Token>) => void;
|
||||
|
||||
export type NoteAddEventHander = (note: Note) => void;
|
||||
export type NoteRemoveEventHander = (noteId: string) => void;
|
||||
|
@ -40,5 +40,5 @@ export type TokenMenuOptions = {
|
||||
export type TokenDraggingOptions = {
|
||||
dragging: boolean;
|
||||
tokenState: TokenState;
|
||||
tokenGroup: Konva.Node;
|
||||
tokenNode: Konva.Node;
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Color } from "../helpers/colors";
|
||||
import { Outline } from "./Outline";
|
||||
import { TokenCategory } from "./Token";
|
||||
|
||||
@ -8,7 +9,7 @@ export type BaseTokenState = {
|
||||
size: number;
|
||||
category: TokenCategory;
|
||||
label: string;
|
||||
statuses: string[];
|
||||
statuses: Color[];
|
||||
x: number;
|
||||
y: number;
|
||||
lastModifiedBy: string;
|
||||
|
Loading…
Reference in New Issue
Block a user