Typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-17 12:48:04 +10:00
parent e48d19a817
commit fecf8090ea
23 changed files with 556 additions and 254 deletions

View File

@ -3,7 +3,7 @@ import Creatable from "react-select/creatable";
import { useThemeUI } from "theme-ui"; import { useThemeUI } from "theme-ui";
type SelectProps = { type SelectProps = {
creatable: boolean; creatable?: boolean;
} & Props; } & Props;
function Select({ creatable, ...props }: SelectProps) { function Select({ creatable, ...props }: SelectProps) {
@ -76,4 +76,8 @@ function Select({ creatable, ...props }: SelectProps) {
); );
} }
Select.defaultProps = {
creatable: false,
};
export default Select; export default Select;

View File

@ -72,29 +72,31 @@ function DragOverlay({ dragging, node, onRemove }: DragOverlayProps) {
} }
}); });
if (!dragging) {
return null;
}
return ( return (
dragging && ( <Box
<Box sx={{
sx={{ position: "absolute",
position: "absolute", bottom: "32px",
bottom: "32px", left: "50%",
left: "50%", borderRadius: "50%",
borderRadius: "50%", transform: isRemoveHovered
transform: isRemoveHovered ? "translateX(-50%) scale(2.0)"
? "translateX(-50%) scale(2.0)" : "translateX(-50%) scale(1.5)",
: "translateX(-50%) scale(1.5)", transition: "transform 250ms ease",
transition: "transform 250ms ease", color: isRemoveHovered ? "primary" : "text",
color: isRemoveHovered ? "primary" : "text", pointerEvents: "none",
pointerEvents: "none", }}
}} bg="overlay"
bg="overlay" ref={removeTokenRef}
ref={removeTokenRef} >
> <IconButton>
<IconButton> <RemoveTokenIcon />
<RemoveTokenIcon /> </IconButton>
</IconButton> </Box>
</Box>
)
); );
} }

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState } from "react";
import { Box } from "theme-ui"; import { Box } from "theme-ui";
import { useToasts } from "react-toast-notifications"; import { useToasts } from "react-toast-notifications";
@ -27,15 +27,48 @@ import {
RemoveStatesAction, RemoveStatesAction,
} from "../../actions"; } from "../../actions";
import Session from "../../network/Session"; import Session from "../../network/Session";
import { Drawing } from "../../types/Drawing"; import { Drawing, DrawingState } from "../../types/Drawing";
import { Fog } from "../../types/Fog"; import { Fog, FogState } from "../../types/Fog";
import { Map, MapToolId } from "../../types/Map"; import { Map, MapActions, MapToolId } from "../../types/Map";
import { MapState } from "../../types/MapState"; import { MapState } from "../../types/MapState";
import { Settings } from "../../types/Settings"; import { Settings } from "../../types/Settings";
import { import {
MapChangeEventHandler, MapChangeEventHandler,
MapResetEventHandler, MapResetEventHandler,
MapTokensStateCreateHandler,
MapTokenStateRemoveHandler,
NoteChangeEventHandler,
NoteRemoveEventHander,
TokenStateChangeEventHandler,
} from "../../types/Events"; } from "../../types/Events";
import Action from "../../actions/Action";
import Konva from "konva";
import { TokenDraggingOptions, TokenMenuOptions } from "../../types/Token";
import { Note, NoteDraggingOptions, NoteMenuOptions } from "../../types/Note";
type MapProps = {
map: Map;
mapState: MapState;
mapActions: MapActions;
onMapTokenStateChange: TokenStateChangeEventHandler;
onMapTokenStateRemove: MapTokenStateRemoveHandler;
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
onMapDraw: (action: Action<DrawingState>) => void;
onMapDrawUndo: () => void;
onMapDrawRedo: () => void;
onFogDraw: (action: Action<FogState>) => void;
onFogDrawUndo: () => void;
onFogDrawRedo: () => void;
onMapNoteChange: NoteChangeEventHandler;
onMapNoteRemove: NoteRemoveEventHander;
allowMapDrawing: boolean;
allowFogDrawing: boolean;
allowMapChange: boolean;
allowNoteEditing: boolean;
disabledTokens: string[];
session: Session;
};
function Map({ function Map({
map, map,
@ -59,29 +92,7 @@ function Map({
allowNoteEditing, allowNoteEditing,
disabledTokens, disabledTokens,
session, session,
}: { }: MapProps) {
map: Map;
mapState: MapState;
mapActions: ;
onMapTokenStateChange: ;
onMapTokenStateRemove: ;
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
onMapDraw: ;
onMapDrawUndo: ;
onMapDrawRedo: ;
onFogDraw: ;
onFogDrawUndo: ;
onFogDrawRedo: ;
onMapNoteChange: ;
onMapNoteRemove: ;
allowMapDrawing: boolean;
allowFogDrawing: boolean;
allowMapChange: boolean;
allowNoteEditing: boolean;
disabledTokens: ;
session: Session;
}) {
const { addToast } = useToasts(); const { addToast } = useToasts();
const { tokensById } = useTokenData(); const { tokensById } = useTokenData();
@ -141,7 +152,7 @@ function Map({
onFogDraw(new EditStatesAction(shapes)); onFogDraw(new EditStatesAction(shapes));
} }
const disabledControls = []; const disabledControls: MapToolId[] = [];
if (!allowMapDrawing) { if (!allowMapDrawing) {
disabledControls.push("drawing"); disabledControls.push("drawing");
} }
@ -206,9 +217,10 @@ function Map({
); );
const [isTokenMenuOpen, setIsTokenMenuOpen] = useState<boolean>(false); const [isTokenMenuOpen, setIsTokenMenuOpen] = useState<boolean>(false);
const [tokenMenuOptions, setTokenMenuOptions] = useState({}); const [tokenMenuOptions, setTokenMenuOptions] = useState<TokenMenuOptions>();
const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); const [tokenDraggingOptions, setTokenDraggingOptions] =
function handleTokenMenuOpen(tokenStateId: string, tokenImage) { useState<TokenDraggingOptions>();
function handleTokenMenuOpen(tokenStateId: string, tokenImage: Konva.Node) {
setTokenMenuOptions({ tokenStateId, tokenImage }); setTokenMenuOptions({ tokenStateId, tokenImage });
setIsTokenMenuOpen(true); setIsTokenMenuOpen(true);
} }
@ -220,7 +232,7 @@ function Map({
tokenDraggingOptions={tokenDraggingOptions} tokenDraggingOptions={tokenDraggingOptions}
setTokenDraggingOptions={setTokenDraggingOptions} setTokenDraggingOptions={setTokenDraggingOptions}
onMapTokenStateChange={onMapTokenStateChange} onMapTokenStateChange={onMapTokenStateChange}
handleTokenMenuOpen={handleTokenMenuOpen} onTokenMenuOpen={handleTokenMenuOpen}
selectedToolId={selectedToolId} selectedToolId={selectedToolId}
disabledTokens={disabledTokens} disabledTokens={disabledTokens}
/> />
@ -231,8 +243,12 @@ function Map({
isOpen={isTokenMenuOpen} isOpen={isTokenMenuOpen}
onRequestClose={() => setIsTokenMenuOpen(false)} onRequestClose={() => setIsTokenMenuOpen(false)}
onTokenStateChange={onMapTokenStateChange} onTokenStateChange={onMapTokenStateChange}
tokenState={mapState && mapState.tokens[tokenMenuOptions.tokenStateId]} tokenState={
tokenImage={tokenMenuOptions.tokenImage} tokenMenuOptions &&
mapState &&
mapState.tokens[tokenMenuOptions.tokenStateId]
}
tokenImage={tokenMenuOptions && tokenMenuOptions.tokenImage}
map={map} map={map}
/> />
); );
@ -241,7 +257,7 @@ function Map({
<TokenDragOverlay <TokenDragOverlay
onTokenStateRemove={(state) => { onTokenStateRemove={(state) => {
onMapTokenStateRemove(state); onMapTokenStateRemove(state);
setTokenDraggingOptions(null); setTokenDraggingOptions(undefined);
}} }}
onTokenStateChange={onMapTokenStateChange} onTokenStateChange={onMapTokenStateChange}
tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState} tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState}
@ -291,14 +307,19 @@ function Map({
); );
const [isNoteMenuOpen, setIsNoteMenuOpen] = useState<boolean>(false); const [isNoteMenuOpen, setIsNoteMenuOpen] = useState<boolean>(false);
const [noteMenuOptions, setNoteMenuOptions] = useState({}); const [noteMenuOptions, setNoteMenuOptions] = useState<NoteMenuOptions>();
const [noteDraggingOptions, setNoteDraggingOptions] = useState(); const [noteDraggingOptions, setNoteDraggingOptions] =
function handleNoteMenuOpen(noteId: string, noteNode) { useState<NoteDraggingOptions>();
function handleNoteMenuOpen(noteId: string, noteNode: Konva.Node) {
setNoteMenuOptions({ noteId, noteNode }); setNoteMenuOptions({ noteId, noteNode });
setIsNoteMenuOpen(true); setIsNoteMenuOpen(true);
} }
function sortNotes(a, b, noteDraggingOptions) { function sortNotes(
a: Note,
b: Note,
noteDraggingOptions?: NoteDraggingOptions
) {
if ( if (
noteDraggingOptions && noteDraggingOptions &&
noteDraggingOptions.dragging && noteDraggingOptions.dragging &&
@ -341,6 +362,7 @@ function Map({
setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target })
} }
onNoteDragEnd={() => onNoteDragEnd={() =>
noteDraggingOptions &&
setNoteDraggingOptions({ ...noteDraggingOptions, dragging: false }) setNoteDraggingOptions({ ...noteDraggingOptions, dragging: false })
} }
fadeOnHover={selectedToolId === "drawing"} fadeOnHover={selectedToolId === "drawing"}
@ -352,23 +374,25 @@ function Map({
isOpen={isNoteMenuOpen} isOpen={isNoteMenuOpen}
onRequestClose={() => setIsNoteMenuOpen(false)} onRequestClose={() => setIsNoteMenuOpen(false)}
onNoteChange={onMapNoteChange} onNoteChange={onMapNoteChange}
note={mapState && mapState.notes[noteMenuOptions.noteId]} note={
noteNode={noteMenuOptions.noteNode} noteMenuOptions && mapState && mapState.notes[noteMenuOptions.noteId]
}
noteNode={noteMenuOptions?.noteNode}
map={map} map={map}
/> />
); );
const noteDragOverlay = ( const noteDragOverlay = noteDraggingOptions ? (
<NoteDragOverlay <NoteDragOverlay
dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)} dragging={noteDraggingOptions.dragging}
noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup} noteGroup={noteDraggingOptions.noteGroup}
noteId={noteDraggingOptions && noteDraggingOptions.noteId} noteId={noteDraggingOptions.noteId}
onNoteRemove={(noteId) => { onNoteRemove={(noteId) => {
onMapNoteRemove(noteId); onMapNoteRemove(noteId);
setNoteDraggingOptions(null); setNoteDraggingOptions(undefined);
}} }}
/> />
); ) : null;
return ( return (
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>

View File

@ -24,8 +24,7 @@ import MapGrid from "./MapGrid";
import MapGridEditor from "./MapGridEditor"; import MapGridEditor from "./MapGridEditor";
import { Map } from "../../types/Map"; import { Map } from "../../types/Map";
import { GridInset } from "../../types/Grid"; import { GridInset } from "../../types/Grid";
import { MapSettingsChangeEventHandler } from "../../types/Events";
type MapSettingsChangeEventHandler = (change: Partial<Map>) => void;
type MapEditorProps = { type MapEditorProps = {
map: Map; map: Map;

View File

@ -9,10 +9,10 @@ type MapMenuProps = {
isOpen: boolean; isOpen: boolean;
onRequestClose: RequestCloseEventHandler; onRequestClose: RequestCloseEventHandler;
onModalContent: (instance: HTMLDivElement) => void; onModalContent: (instance: HTMLDivElement) => void;
top: number; top: number | string;
left: number; left: number | string;
bottom: number; bottom: number | string;
right: number; right: number | string;
children: React.ReactNode; children: React.ReactNode;
style: React.CSSProperties; style: React.CSSProperties;
excludeNode: Node | null; excludeNode: Node | null;

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import shortid from "shortid"; import shortid from "shortid";
import { Group } from "react-konva"; import { Group } from "react-konva";
import Konva from "konva";
import { useInteractionEmitter } from "../../contexts/MapInteractionContext"; import { useInteractionEmitter } from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext"; import { useMapStage } from "../../contexts/MapStageContext";
@ -13,8 +14,30 @@ import useGridSnapping from "../../hooks/useGridSnapping";
import Note from "../note/Note"; import Note from "../note/Note";
import { Map } from "../../types/Map";
import { Note as NoteType } from "../../types/Note";
import {
NoteAddEventHander,
NoteChangeEventHandler,
NoteDragEventHandler,
NoteMenuOpenEventHandler,
} from "../../types/Events";
const defaultNoteSize = 2; const defaultNoteSize = 2;
type MapNoteProps = {
map: Map;
active: boolean;
onNoteAdd: NoteAddEventHander;
onNoteChange: NoteChangeEventHandler;
notes: NoteType[];
onNoteMenuOpen: NoteMenuOpenEventHandler;
draggable: boolean;
onNoteDragStart: NoteDragEventHandler;
onNoteDragEnd: NoteDragEventHandler;
fadeOnHover: boolean;
};
function MapNotes({ function MapNotes({
map, map,
active, active,
@ -26,14 +49,14 @@ function MapNotes({
onNoteDragStart, onNoteDragStart,
onNoteDragEnd, onNoteDragEnd,
fadeOnHover, fadeOnHover,
}) { }: MapNoteProps) {
const interactionEmitter = useInteractionEmitter(); const interactionEmitter = useInteractionEmitter();
const userId = useUserId(); const userId = useUserId();
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
const [isBrushDown, setIsBrushDown] = useState(false); const [isBrushDown, setIsBrushDown] = useState(false);
const [noteData, setNoteData] = useState(null); const [noteData, setNoteData] = useState<NoteType | null>(null);
const creatingNoteRef = useRef(); const creatingNoteRef = useRef<Konva.Group>(null);
const snapPositionToGrid = useGridSnapping(); const snapPositionToGrid = useGridSnapping();
@ -44,8 +67,14 @@ function MapNotes({
const mapStage = mapStageRef.current; const mapStage = mapStageRef.current;
function getBrushPosition() { function getBrushPosition() {
if (!mapStage) {
return;
}
const mapImage = mapStage.findOne("#mapImage"); const mapImage = mapStage.findOne("#mapImage");
let position = getRelativePointerPosition(mapImage); let position = getRelativePointerPosition(mapImage);
if (!position) {
return;
}
if (map.snapToGrid) { if (map.snapToGrid) {
position = snapPositionToGrid(position); position = snapPositionToGrid(position);
} }
@ -57,6 +86,9 @@ function MapNotes({
function handleBrushDown() { function handleBrushDown() {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (!brushPosition || !userId) {
return;
}
setNoteData({ setNoteData({
x: brushPosition.x, x: brushPosition.x,
y: brushPosition.y, y: brushPosition.y,
@ -76,17 +108,25 @@ function MapNotes({
function handleBrushMove() { function handleBrushMove() {
if (noteData) { if (noteData) {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
setNoteData((prev) => ({ if (!brushPosition) {
...prev, return;
x: brushPosition.x, }
y: brushPosition.y, setNoteData((prev) => {
})); if (!prev) {
return prev;
}
return {
...prev,
x: brushPosition.x,
y: brushPosition.y,
};
});
setIsBrushDown(true); setIsBrushDown(true);
} }
} }
function handleBrushUp() { function handleBrushUp() {
if (noteData) { if (noteData && creatingNoteRef.current) {
onNoteAdd(noteData); onNoteAdd(noteData);
onNoteMenuOpen(noteData.id, creatingNoteRef.current); onNoteMenuOpen(noteData.id, creatingNoteRef.current);
} }
@ -94,14 +134,14 @@ function MapNotes({
setIsBrushDown(false); setIsBrushDown(false);
} }
interactionEmitter.on("dragStart", handleBrushDown); interactionEmitter?.on("dragStart", handleBrushDown);
interactionEmitter.on("drag", handleBrushMove); interactionEmitter?.on("drag", handleBrushMove);
interactionEmitter.on("dragEnd", handleBrushUp); interactionEmitter?.on("dragEnd", handleBrushUp);
return () => { return () => {
interactionEmitter.off("dragStart", handleBrushDown); interactionEmitter?.off("dragStart", handleBrushDown);
interactionEmitter.off("drag", handleBrushMove); interactionEmitter?.off("drag", handleBrushMove);
interactionEmitter.off("dragEnd", handleBrushUp); interactionEmitter?.off("dragEnd", handleBrushUp);
}; };
}); });
@ -121,9 +161,7 @@ function MapNotes({
/> />
))} ))}
<Group ref={creatingNoteRef}> <Group ref={creatingNoteRef}>
{isBrushDown && noteData && ( {isBrushDown && noteData && <Note note={noteData} map={map} />}
<Note note={noteData} map={map} draggable={false} />
)}
</Group> </Group>
</Group> </Group>
); );

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import { useEffect } from "react";
import { Group } from "react-konva"; import { Group } from "react-konva";
import { import {
@ -15,7 +15,17 @@ import {
} from "../../helpers/konva"; } from "../../helpers/konva";
import Vector2 from "../../helpers/Vector2"; import Vector2 from "../../helpers/Vector2";
import colors from "../../helpers/colors"; import colors, { Color } from "../../helpers/colors";
type MapPointerProps = {
active: boolean;
position: Vector2;
onPointerDown?: (position: Vector2) => void;
onPointerMove?: (position: Vector2) => void;
onPointerUp?: (position: Vector2) => void;
visible: boolean;
color: Color;
};
function MapPointer({ function MapPointer({
active, active,
@ -25,7 +35,7 @@ function MapPointer({
onPointerUp, onPointerUp,
visible, visible,
color, color,
}) { }: MapPointerProps) {
const mapWidth = useMapWidth(); const mapWidth = useMapWidth();
const mapHeight = useMapHeight(); const mapHeight = useMapHeight();
const interactionEmitter = useInteractionEmitter(); const interactionEmitter = useInteractionEmitter();
@ -40,30 +50,36 @@ function MapPointer({
const mapStage = mapStageRef.current; const mapStage = mapStageRef.current;
function getBrushPosition() { function getBrushPosition() {
if (!mapStage) {
return;
}
const mapImage = mapStage.findOne("#mapImage"); const mapImage = mapStage.findOne("#mapImage");
return getRelativePointerPositionNormalized(mapImage); return getRelativePointerPositionNormalized(mapImage);
} }
function handleBrushDown() { function handleBrushDown() {
onPointerDown && onPointerDown(getBrushPosition()); const brushPosition = getBrushPosition();
brushPosition && onPointerDown?.(brushPosition);
} }
function handleBrushMove() { function handleBrushMove() {
onPointerMove && visible && onPointerMove(getBrushPosition()); const brushPosition = getBrushPosition();
brushPosition && visible && onPointerMove?.(brushPosition);
} }
function handleBrushUp() { function handleBrushUp() {
onPointerMove && onPointerUp(getBrushPosition()); const brushPosition = getBrushPosition();
brushPosition && onPointerUp?.(brushPosition);
} }
interactionEmitter.on("dragStart", handleBrushDown); interactionEmitter?.on("dragStart", handleBrushDown);
interactionEmitter.on("drag", handleBrushMove); interactionEmitter?.on("drag", handleBrushMove);
interactionEmitter.on("dragEnd", handleBrushUp); interactionEmitter?.on("dragEnd", handleBrushUp);
return () => { return () => {
interactionEmitter.off("dragStart", handleBrushDown); interactionEmitter?.off("dragStart", handleBrushDown);
interactionEmitter.off("drag", handleBrushMove); interactionEmitter?.off("drag", handleBrushMove);
interactionEmitter.off("dragEnd", handleBrushUp); interactionEmitter?.off("dragEnd", handleBrushUp);
}; };
}); });

View File

@ -9,8 +9,16 @@ import { mapSources as defaultMapSources } from "../../maps";
import Divider from "../Divider"; import Divider from "../Divider";
import Select from "../Select"; import Select from "../Select";
import { Map, MapQuality } from "../../types/Map";
import { EditFlag, MapState } from "../../types/MapState";
import {
MapSettingsChangeEventHandler,
MapStateSettingsChangeEventHandler,
} from "../../types/Events";
import { Grid, GridMeasurementType, GridType } from "../../types/Grid";
const qualitySettings = [ type QualityTypeSetting = { value: MapQuality; label: string };
const qualitySettings: QualityTypeSetting[] = [
{ value: "low", label: "Low" }, { value: "low", label: "Low" },
{ value: "medium", label: "Medium" }, { value: "medium", label: "Medium" },
{ value: "high", label: "High" }, { value: "high", label: "High" },
@ -18,42 +26,53 @@ const qualitySettings = [
{ value: "original", label: "Original" }, { value: "original", label: "Original" },
]; ];
const gridTypeSettings = [ type GridTypeSetting = { value: GridType; label: string };
const gridTypeSettings: GridTypeSetting[] = [
{ value: "square", label: "Square" }, { value: "square", label: "Square" },
{ value: "hexVertical", label: "Hex Vertical" }, { value: "hexVertical", label: "Hex Vertical" },
{ value: "hexHorizontal", label: "Hex Horizontal" }, { value: "hexHorizontal", label: "Hex Horizontal" },
]; ];
const gridSquareMeasurementTypeSettings = [ type GridMeasurementTypeSetting = { value: GridMeasurementType; label: string };
const gridSquareMeasurementTypeSettings: GridMeasurementTypeSetting[] = [
{ value: "chebyshev", label: "Chessboard (D&D 5e)" }, { value: "chebyshev", label: "Chessboard (D&D 5e)" },
{ value: "alternating", label: "Alternating Diagonal (D&D 3.5e)" }, { value: "alternating", label: "Alternating Diagonal (D&D 3.5e)" },
{ value: "euclidean", label: "Euclidean" }, { value: "euclidean", label: "Euclidean" },
{ value: "manhattan", label: "Manhattan" }, { value: "manhattan", label: "Manhattan" },
]; ];
const gridHexMeasurementTypeSettings = [ const gridHexMeasurementTypeSettings: GridMeasurementTypeSetting[] = [
{ value: "manhattan", label: "Manhattan" }, { value: "manhattan", label: "Manhattan" },
{ value: "euclidean", label: "Euclidean" }, { value: "euclidean", label: "Euclidean" },
]; ];
type MapSettingsProps = {
map: Map;
mapState: MapState;
onSettingsChange: MapSettingsChangeEventHandler;
onStateSettingsChange: MapStateSettingsChangeEventHandler;
};
function MapSettings({ function MapSettings({
map, map,
mapState, mapState,
onSettingsChange, onSettingsChange,
onStateSettingsChange, onStateSettingsChange,
}) { }: MapSettingsProps) {
function handleFlagChange(event, flag) { function handleFlagChange(
event: React.ChangeEvent<HTMLInputElement>,
flag: EditFlag
) {
if (event.target.checked) { if (event.target.checked) {
onStateSettingsChange("editFlags", [...mapState.editFlags, flag]); onStateSettingsChange({ editFlags: [...mapState.editFlags, flag] });
} else { } else {
onStateSettingsChange( onStateSettingsChange({
"editFlags", editFlags: mapState.editFlags.filter((f) => f !== flag),
mapState.editFlags.filter((f) => f !== flag) });
);
} }
} }
function handleGridSizeXChange(event) { function handleGridSizeXChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseInt(event.target.value) || 0; const value = parseInt(event.target.value) || 0;
let grid = { let grid = {
...map.grid, ...map.grid,
@ -63,10 +82,10 @@ function MapSettings({
}, },
}; };
grid.inset = getGridUpdatedInset(grid, map.width, map.height); grid.inset = getGridUpdatedInset(grid, map.width, map.height);
onSettingsChange("grid", grid); onSettingsChange({ grid });
} }
function handleGridSizeYChange(event) { function handleGridSizeYChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseInt(event.target.value) || 0; const value = parseInt(event.target.value) || 0;
let grid = { let grid = {
...map.grid, ...map.grid,
@ -76,12 +95,15 @@ function MapSettings({
}, },
}; };
grid.inset = getGridUpdatedInset(grid, map.width, map.height); grid.inset = getGridUpdatedInset(grid, map.width, map.height);
onSettingsChange("grid", grid); onSettingsChange({ grid });
} }
function handleGridTypeChange(option) { function handleGridTypeChange(option: GridTypeSetting | null) {
if (!option) {
return;
}
const type = option.value; const type = option.value;
let grid = { let grid: Grid = {
...map.grid, ...map.grid,
type, type,
measurement: { measurement: {
@ -90,10 +112,15 @@ function MapSettings({
}, },
}; };
grid.inset = getGridUpdatedInset(grid, map.width, map.height); grid.inset = getGridUpdatedInset(grid, map.width, map.height);
onSettingsChange("grid", grid); onSettingsChange({ grid });
} }
function handleGridMeasurementTypeChange(option) { function handleGridMeasurementTypeChange(
option: GridMeasurementTypeSetting | null
) {
if (!option) {
return;
}
const grid = { const grid = {
...map.grid, ...map.grid,
measurement: { measurement: {
@ -101,10 +128,19 @@ function MapSettings({
type: option.value, type: option.value,
}, },
}; };
onSettingsChange("grid", grid); onSettingsChange({ grid });
} }
function handleGridMeasurementScaleChange(event) { function handleQualityChange(option: QualityTypeSetting | null) {
if (!option) {
return;
}
onSettingsChange({ quality: option.value });
}
function handleGridMeasurementScaleChange(
event: React.ChangeEvent<HTMLInputElement>
) {
const grid = { const grid = {
...map.grid, ...map.grid,
measurement: { measurement: {
@ -112,7 +148,7 @@ function MapSettings({
scale: event.target.value, scale: event.target.value,
}, },
}; };
onSettingsChange("grid", grid); onSettingsChange({ grid });
} }
const mapURL = useDataURL(map, defaultMapSources); const mapURL = useDataURL(map, defaultMapSources);
@ -124,7 +160,7 @@ function MapSettings({
const blob = await response.blob(); const blob = await response.blob();
let size = blob.size; let size = blob.size;
size /= 1000000; // Bytes to Megabytes size /= 1000000; // Bytes to Megabytes
setMapSize(size.toFixed(2)); setMapSize(parseFloat(size.toFixed(2)));
} else { } else {
setMapSize(0); setMapSize(0);
} }
@ -168,7 +204,7 @@ function MapSettings({
<Input <Input
name="name" name="name"
value={(map && map.name) || ""} value={(map && map.name) || ""}
onChange={(e) => onSettingsChange("name", e.target.value)} onChange={(e) => onSettingsChange({ name: e.target.value })}
disabled={mapEmpty} disabled={mapEmpty}
my={1} my={1}
/> />
@ -185,10 +221,11 @@ function MapSettings({
isDisabled={mapEmpty} isDisabled={mapEmpty}
options={gridTypeSettings} options={gridTypeSettings}
value={ value={
!mapEmpty && mapEmpty
gridTypeSettings.find((s) => s.value === map.grid.type) ? undefined
: gridTypeSettings.find((s) => s.value === map.grid.type)
} }
onChange={handleGridTypeChange} onChange={handleGridTypeChange as any}
isSearchable={false} isSearchable={false}
/> />
</Box> </Box>
@ -197,7 +234,9 @@ function MapSettings({
<Checkbox <Checkbox
checked={!mapEmpty && map.showGrid} checked={!mapEmpty && map.showGrid}
disabled={mapEmpty} disabled={mapEmpty}
onChange={(e) => onSettingsChange("showGrid", e.target.checked)} onChange={(e) =>
onSettingsChange({ showGrid: e.target.checked })
}
/> />
Draw Grid Draw Grid
</Label> </Label>
@ -206,7 +245,7 @@ function MapSettings({
checked={!mapEmpty && map.snapToGrid} checked={!mapEmpty && map.snapToGrid}
disabled={mapEmpty} disabled={mapEmpty}
onChange={(e) => onChange={(e) =>
onSettingsChange("snapToGrid", e.target.checked) onSettingsChange({ snapToGrid: e.target.checked })
} }
/> />
Snap to Grid Snap to Grid
@ -224,12 +263,13 @@ function MapSettings({
: gridHexMeasurementTypeSettings : gridHexMeasurementTypeSettings
} }
value={ value={
!mapEmpty && mapEmpty
gridSquareMeasurementTypeSettings.find( ? undefined
(s) => s.value === map.grid.measurement.type : gridSquareMeasurementTypeSettings.find(
) (s) => s.value === map.grid.measurement.type
)
} }
onChange={handleGridMeasurementTypeChange} onChange={handleGridMeasurementTypeChange as any}
isSearchable={false} isSearchable={false}
/> />
</Box> </Box>
@ -254,14 +294,17 @@ function MapSettings({
<Select <Select
options={qualitySettings} options={qualitySettings}
value={ value={
!mapEmpty && mapEmpty
qualitySettings.find((s) => s.value === map.quality) ? undefined
: qualitySettings.find((s) => s.value === map.quality)
} }
isDisabled={mapEmpty} isDisabled={mapEmpty}
onChange={(option) => onSettingsChange("quality", option.value)} onChange={handleQualityChange as any}
isOptionDisabled={(option) => isOptionDisabled={
mapEmpty || ((option: QualityTypeSetting) =>
(option.value !== "original" && !map.resolutions[option.value]) mapEmpty ||
(option.value !== "original" &&
!map.resolutions[option.value])) as any
} }
isSearchable={false} isSearchable={false}
/> />

View File

@ -1,8 +1,19 @@
import React from "react"; import React from "react";
import { Map } from "../../types/Map";
import Tile from "../tile/Tile"; import Tile from "../tile/Tile";
import MapImage from "./MapTileImage"; import MapImage from "./MapTileImage";
type MapTileProps = {
map: Map;
isSelected: boolean;
onSelect: (mapId: string) => void;
onEdit: (mapId: string) => void;
onDoubleClick: () => void;
canEdit: boolean;
badges: React.ReactChild[];
};
function MapTile({ function MapTile({
map, map,
isSelected, isSelected,
@ -11,7 +22,7 @@ function MapTile({
onDoubleClick, onDoubleClick,
canEdit, canEdit,
badges, badges,
}) { }: MapTileProps) {
return ( return (
<Tile <Tile
title={map.name} title={map.name}

View File

@ -6,7 +6,24 @@ import MapImage from "./MapTileImage";
import useResponsiveLayout from "../../hooks/useResponsiveLayout"; import useResponsiveLayout from "../../hooks/useResponsiveLayout";
function MapTileGroup({ group, maps, isSelected, onSelect, onDoubleClick }) { import { Map } from "../../types/Map";
import { GroupContainer } from "../../types/Group";
type MapTileGroupProps = {
group: GroupContainer;
maps: Map[];
isSelected: boolean;
onSelect: (groupId: string) => void;
onDoubleClick: () => void;
};
function MapTileGroup({
group,
maps,
isSelected,
onSelect,
onDoubleClick,
}: MapTileGroupProps) {
const layout = useResponsiveLayout(); const layout = useResponsiveLayout();
return ( return (

View File

@ -10,11 +10,26 @@ import { getGroupItems } from "../../helpers/group";
import { useGroup } from "../../contexts/GroupContext"; import { useGroup } from "../../contexts/GroupContext";
function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) { import { Map } from "../../types/Map";
import { Group } from "../../types/Group";
type MapTileProps = {
mapsById: Record<string, Map>;
onMapEdit: (mapId: string) => void;
onMapSelect: (groupId: string) => void;
subgroup: boolean;
};
function MapTiles({
mapsById,
onMapEdit,
onMapSelect,
subgroup,
}: MapTileProps) {
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 map = mapsById[group.id]; const map = mapsById[group.id];
if (map) { if (map) {

View File

@ -31,7 +31,7 @@ import {
TokenStateChangeEventHandler, TokenStateChangeEventHandler,
} from "../../types/Events"; } from "../../types/Events";
type MapTokenStateProps = { type MapTokenProps = {
tokenState: TokenState; tokenState: TokenState;
onTokenStateChange: TokenStateChangeEventHandler; onTokenStateChange: TokenStateChangeEventHandler;
onTokenMenuOpen: TokenMenuOpenChangeEventHandler; onTokenMenuOpen: TokenMenuOpenChangeEventHandler;
@ -51,7 +51,7 @@ function MapToken({
draggable, draggable,
fadeOnHover, fadeOnHover,
map, map,
}: MapTokenStateProps) { }: MapTokenProps) {
const userId = useUserId(); const userId = useUserId();
const mapWidth = useMapWidth(); const mapWidth = useMapWidth();

View File

@ -1,19 +1,37 @@
import React from "react";
import { Group } from "react-konva"; import { Group } from "react-konva";
import {
TokenMenuOpenChangeEventHandler,
TokenStateChangeEventHandler,
} from "../../types/Events";
import { Map, MapToolId } from "../../types/Map";
import { MapState } from "../../types/MapState";
import { TokenCategory, TokenDraggingOptions } from "../../types/Token";
import { TokenState } from "../../types/TokenState";
import MapToken from "./MapToken"; import MapToken from "./MapToken";
type MapTokensProps = {
map: Map;
mapState: MapState;
tokenDraggingOptions?: TokenDraggingOptions;
setTokenDraggingOptions: (options: TokenDraggingOptions) => void;
onMapTokenStateChange: TokenStateChangeEventHandler;
onTokenMenuOpen: TokenMenuOpenChangeEventHandler;
selectedToolId: MapToolId;
disabledTokens: string[];
};
function MapTokens({ function MapTokens({
map, map,
mapState, mapState,
tokenDraggingOptions, tokenDraggingOptions,
setTokenDraggingOptions, setTokenDraggingOptions,
onMapTokenStateChange, onMapTokenStateChange,
handleTokenMenuOpen, onTokenMenuOpen,
selectedToolId, selectedToolId,
disabledTokens, disabledTokens,
}) { }: MapTokensProps) {
function getMapTokenCategoryWeight(category) { function getMapTokenCategoryWeight(category: TokenCategory) {
switch (category) { switch (category) {
case "character": case "character":
return 0; return 0;
@ -27,7 +45,11 @@ function MapTokens({
} }
// Sort so vehicles render below other tokens // Sort so vehicles render below other tokens
function sortMapTokenStates(a, b, tokenDraggingOptions) { function sortMapTokenStates(
a: TokenState,
b: TokenState,
tokenDraggingOptions?: TokenDraggingOptions
) {
// If categories are different sort in order "prop", "vehicle", "character" // If categories are different sort in order "prop", "vehicle", "character"
if (b.category !== a.category) { if (b.category !== a.category) {
const aWeight = getMapTokenCategoryWeight(a.category); const aWeight = getMapTokenCategoryWeight(a.category);
@ -62,7 +84,7 @@ function MapTokens({
key={tokenState.id} key={tokenState.id}
tokenState={tokenState} tokenState={tokenState}
onTokenStateChange={onMapTokenStateChange} onTokenStateChange={onMapTokenStateChange}
onTokenMenuOpen={handleTokenMenuOpen} onTokenMenuOpen={onTokenMenuOpen}
onTokenDragStart={(e) => onTokenDragStart={(e) =>
setTokenDraggingOptions({ setTokenDraggingOptions({
dragging: true, dragging: true,
@ -71,6 +93,7 @@ function MapTokens({
}) })
} }
onTokenDragEnd={() => onTokenDragEnd={() =>
tokenDraggingOptions &&
setTokenDraggingOptions({ setTokenDraggingOptions({
...tokenDraggingOptions, ...tokenDraggingOptions,
dragging: false, dragging: false,

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { Rect, Text } from "react-konva"; import { Rect, Text } from "react-konva";
import Konva from "konva";
import { useSpring, animated } from "@react-spring/konva"; import { useSpring, animated } from "@react-spring/konva";
import { useUserId } from "../../contexts/UserIdContext"; import { useUserId } from "../../contexts/UserIdContext";
@ -15,8 +16,27 @@ import colors from "../../helpers/colors";
import usePrevious from "../../hooks/usePrevious"; import usePrevious from "../../hooks/usePrevious";
import useGridSnapping from "../../hooks/useGridSnapping"; import useGridSnapping from "../../hooks/useGridSnapping";
import { Note as NoteType } from "../../types/Note";
import {
NoteChangeEventHandler,
NoteDragEventHandler,
NoteMenuOpenEventHandler,
} from "../../types/Events";
import { Map } from "../../types/Map";
const defaultFontSize = 16; const defaultFontSize = 16;
type NoteProps = {
note: NoteType;
map: Map;
onNoteChange?: NoteChangeEventHandler;
onNoteMenuOpen?: NoteMenuOpenEventHandler;
draggable: boolean;
onNoteDragStart?: NoteDragEventHandler;
onNoteDragEnd?: NoteDragEventHandler;
fadeOnHover: boolean;
};
function Note({ function Note({
note, note,
map, map,
@ -26,7 +46,7 @@ function Note({
onNoteDragStart, onNoteDragStart,
onNoteDragEnd, onNoteDragEnd,
fadeOnHover, fadeOnHover,
}) { }: NoteProps) {
const userId = useUserId(); const userId = useUserId();
const mapWidth = useMapWidth(); const mapWidth = useMapWidth();
@ -45,11 +65,11 @@ function Note({
const snapPositionToGrid = useGridSnapping(); const snapPositionToGrid = useGridSnapping();
function handleDragStart(event) { function handleDragStart(event: Konva.KonvaEventObject<DragEvent>) {
onNoteDragStart && onNoteDragStart(event, note.id); onNoteDragStart?.(event, note.id);
} }
function handleDragMove(event) { function handleDragMove(event: Konva.KonvaEventObject<DragEvent>) {
const noteGroup = event.target; const noteGroup = event.target;
// Snap to corners of grid // Snap to corners of grid
if (map.snapToGrid) { if (map.snapToGrid) {
@ -57,21 +77,20 @@ function Note({
} }
} }
function handleDragEnd(event) { function handleDragEnd(event: Konva.KonvaEventObject<DragEvent>) {
const noteGroup = event.target; const noteGroup = event.target;
onNoteChange && onNoteChange?.({
onNoteChange({ ...note,
...note, x: noteGroup.x() / mapWidth,
x: noteGroup.x() / mapWidth, y: noteGroup.y() / mapHeight,
y: noteGroup.y() / mapHeight, lastModifiedBy: userId,
lastModifiedBy: userId, lastModified: Date.now(),
lastModified: Date.now(), });
}); onNoteDragEnd?.(event, note.id);
onNoteDragEnd && onNoteDragEnd(note.id);
setPreventMapInteraction(false); setPreventMapInteraction(false);
} }
function handleClick(event) { function handleClick(event: Konva.KonvaEventObject<MouseEvent>) {
if (draggable) { if (draggable) {
const noteNode = event.target; const noteNode = event.target;
onNoteMenuOpen && onNoteMenuOpen(note.id, noteNode); onNoteMenuOpen && onNoteMenuOpen(note.id, noteNode);
@ -79,8 +98,8 @@ function Note({
} }
// Store note pointer down time to check for a click when note is locked // Store note pointer down time to check for a click when note is locked
const notePointerDownTimeRef = useRef(); const notePointerDownTimeRef = useRef<number>(0);
function handlePointerDown(event) { function handlePointerDown(event: Konva.KonvaEventObject<PointerEvent>) {
if (draggable) { if (draggable) {
setPreventMapInteraction(true); setPreventMapInteraction(true);
} }
@ -89,7 +108,7 @@ function Note({
} }
} }
function handlePointerUp(event) { function handlePointerUp(event: Konva.KonvaEventObject<PointerEvent>) {
if (draggable) { if (draggable) {
setPreventMapInteraction(false); setPreventMapInteraction(false);
} }
@ -100,7 +119,7 @@ function Note({
const delta = event.evt.timeStamp - notePointerDownTimeRef.current; const delta = event.evt.timeStamp - notePointerDownTimeRef.current;
if (delta < 300) { if (delta < 300) {
const noteNode = event.target; const noteNode = event.target;
onNoteMenuOpen(note.id, noteNode); onNoteMenuOpen?.(note.id, noteNode);
} }
} }
} }
@ -121,12 +140,10 @@ function Note({
const [fontScale, setFontScale] = useState(1); const [fontScale, setFontScale] = useState(1);
useEffect(() => { useEffect(() => {
const text = textRef.current; const text = textRef.current;
if (!text) {
return;
}
function findFontSize() { function findFontSize() {
if (!text) {
return;
}
// Create an array from 1 / defaultFontSize of the note height to the full note height // Create an array from 1 / defaultFontSize of the note height to the full note height
let sizes = Array.from( let sizes = Array.from(
{ length: Math.ceil(noteHeight - notePadding * 2) }, { length: Math.ceil(noteHeight - notePadding * 2) },
@ -151,7 +168,7 @@ function Note({
findFontSize(); findFontSize();
}, [note, note.text, note.visible, noteWidth, noteHeight, notePadding]); }, [note, note.text, note.visible, noteWidth, noteHeight, notePadding]);
const textRef = useRef(); const textRef = useRef<Konva.Text>(null);
// Animate to new note positions if edited by others // Animate to new note positions if edited by others
const noteX = note.x * mapWidth; const noteX = note.x * mapWidth;
@ -229,4 +246,9 @@ function Note({
); );
} }
Note.defaultProps = {
fadeOnHover: false,
draggable: false,
};
export default Note; export default Note;

View File

@ -1,19 +0,0 @@
import React from "react";
import DragOverlay from "../map/DragOverlay";
function NoteDragOverlay({ onNoteRemove, noteId, noteGroup, dragging }) {
function handleNoteRemove() {
onNoteRemove(noteId);
}
return (
<DragOverlay
dragging={dragging}
onRemove={handleNoteRemove}
node={noteGroup}
/>
);
}
export default NoteDragOverlay;

View File

@ -0,0 +1,31 @@
import Konva from "konva";
import DragOverlay from "../map/DragOverlay";
type NoteDragOverlayProps = {
onNoteRemove: (noteId: string) => void;
noteId: string;
noteGroup: Konva.Node;
dragging: boolean;
};
function NoteDragOverlay({
onNoteRemove,
noteId,
noteGroup,
dragging,
}: NoteDragOverlayProps) {
function handleNoteRemove() {
onNoteRemove(noteId);
}
return (
<DragOverlay
dragging={dragging}
onRemove={handleNoteRemove}
node={noteGroup}
/>
);
}
export default NoteDragOverlay;

View File

@ -1,12 +1,13 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Box, Flex, Text, IconButton } from "theme-ui"; import { Box, Flex, Text, IconButton } from "theme-ui";
import Konva from "konva";
import Slider from "../Slider"; import Slider from "../Slider";
import TextareaAutosize from "../TextareaAutoSize"; import TextareaAutosize from "../TextareaAutoSize";
import MapMenu from "../map/MapMenu"; import MapMenu from "../map/MapMenu";
import colors, { colorOptions } from "../../helpers/colors"; import colors, { Color, colorOptions } from "../../helpers/colors";
import usePrevious from "../../hooks/usePrevious"; import usePrevious from "../../hooks/usePrevious";
@ -19,8 +20,24 @@ import TextIcon from "../../icons/NoteTextIcon";
import { useUserId } from "../../contexts/UserIdContext"; import { useUserId } from "../../contexts/UserIdContext";
import {
NoteChangeEventHandler,
RequestCloseEventHandler,
} from "../../types/Events";
import { Note } from "../../types/Note";
import { Map } from "../../types/Map";
const defaultNoteMaxSize = 6; const defaultNoteMaxSize = 6;
type NoteMenuProps = {
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
note?: Note;
noteNode?: Konva.Node;
onNoteChange: NoteChangeEventHandler;
map: Map;
};
function NoteMenu({ function NoteMenu({
isOpen, isOpen,
onRequestClose, onRequestClose,
@ -28,7 +45,7 @@ function NoteMenu({
noteNode, noteNode,
onNoteChange, onNoteChange,
map, map,
}) { }: NoteMenuProps) {
const userId = useUserId(); const userId = useUserId();
const wasOpen = usePrevious(isOpen); const wasOpen = usePrevious(isOpen);
@ -43,29 +60,30 @@ function NoteMenu({
if (noteNode) { if (noteNode) {
const nodeRect = noteNode.getClientRect(); const nodeRect = noteNode.getClientRect();
const mapElement = document.querySelector(".map"); const mapElement = document.querySelector(".map");
const mapRect = mapElement.getBoundingClientRect(); if (mapElement) {
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 + nodeRect.x + nodeRect.width / 2 - 156 / 2); setMenuLeft(mapRect.left + nodeRect.x + nodeRect.width / 2 - 156 / 2);
// Y 12px from the bottom // Y 12px from the bottom
setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 12); setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 12);
}
} }
} }
}, [isOpen, note, wasOpen, noteNode]); }, [isOpen, note, wasOpen, noteNode]);
function handleTextChange(event) { function handleTextChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
const text = event.target.value.substring(0, 1024); const text = event.target.value.substring(0, 1024);
note && onNoteChange({ ...note, text: text }); note && onNoteChange({ ...note, text: text });
} }
function handleColorChange(color) { function handleColorChange(color: Color) {
if (!note) { if (!note) {
return; return;
} }
onNoteChange({ ...note, color: color }); onNoteChange({ ...note, color: color });
} }
function handleSizeChange(event) { function handleSizeChange(event: React.ChangeEvent<HTMLInputElement>) {
const newSize = parseFloat(event.target.value); const newSize = parseFloat(event.target.value);
note && onNoteChange({ ...note, size: newSize }); note && onNoteChange({ ...note, size: newSize });
} }
@ -82,30 +100,35 @@ function NoteMenu({
note && onNoteChange({ ...note, textOnly: !note.textOnly }); note && onNoteChange({ ...note, textOnly: !note.textOnly });
} }
function handleModalContent(node) { function handleModalContent(node: HTMLElement) {
if (node) { if (node) {
// Focus input // Focus input
const tokenLabelInput = node.querySelector("#changeNoteText"); const tokenLabelInput =
tokenLabelInput.focus(); node.querySelector<HTMLInputElement>("#changeNoteText");
tokenLabelInput.select(); if (tokenLabelInput) {
tokenLabelInput.focus();
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");
const mapRect = mapElement.getBoundingClientRect(); if (mapElement) {
setMenuLeft((prevLeft) => const mapRect = mapElement.getBoundingClientRect();
Math.min( setMenuLeft((prevLeft) =>
mapRect.right - nodeRect.width, Math.min(
Math.max(mapRect.left, prevLeft) mapRect.right - nodeRect.width,
) Math.max(mapRect.left, prevLeft)
); )
setMenuTop((prevTop) => );
Math.min(mapRect.bottom - nodeRect.height, prevTop) setMenuTop((prevTop) =>
); Math.min(mapRect.bottom - nodeRect.height, prevTop)
);
}
} }
} }
function handleTextKeyPress(e) { function handleTextKeyPress(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
onRequestClose(); onRequestClose();

View File

@ -23,33 +23,21 @@ import TokenBar from "../components/token/TokenBar";
import GlobalImageDrop from "../components/image/GlobalImageDrop"; import GlobalImageDrop from "../components/image/GlobalImageDrop";
import { Map as MapType } from "../types/Map"; import {
Map as MapType,
MapActions,
MapActionsIndexKey,
MapActionsKey,
} from "../types/Map";
import { MapState } from "../types/MapState"; import { MapState } from "../types/MapState";
import { import {
Asset,
AssetManifest, AssetManifest,
AssetManifestAsset, AssetManifestAsset,
AssetManifestAssets, AssetManifestAssets,
} from "../types/Asset"; } from "../types/Asset";
import { TokenState } from "../types/TokenState"; import { TokenState } from "../types/TokenState";
import { Drawing, DrawingState } from "../types/Drawing"; import { DrawingState } from "../types/Drawing";
import { Fog, FogState } from "../types/Fog"; import { FogState } from "../types/Fog";
type MapActions = {
mapDrawActions: Action<Drawing>[];
mapDrawActionIndex: number;
fogDrawActions: Action<Fog>[];
fogDrawActionIndex: number;
};
type MapActionsKey = keyof Pick<
MapActions,
"mapDrawActions" | "fogDrawActions"
>;
type MapActionsIndexKey = keyof Pick<
MapActions,
"mapDrawActionIndex" | "fogDrawActionIndex"
>;
const defaultMapActions: MapActions = { const defaultMapActions: MapActions = {
mapDrawActions: [], mapDrawActions: [],

View File

@ -2,22 +2,39 @@ import Konva from "konva";
import { DefaultDice } from "./Dice"; 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 { TokenState } from "./TokenState"; import { TokenState } from "./TokenState";
export type MapChangeEventHandler = (map?: Map, mapState?: MapState) => void; export type MapChangeEventHandler = (map?: Map, mapState?: MapState) => void;
export type MapResetEventHandler = (newState: MapState) => void; export type MapResetEventHandler = (newState: MapState) => void;
export type MapSettingsChangeEventHandler = (change: Partial<Map>) => void;
export type MapStateSettingsChangeEventHandler = (
change: Partial<MapState>
) => void;
export type DiceSelectEventHandler = (dice: DefaultDice) => void; export type DiceSelectEventHandler = (dice: DefaultDice) => void;
export type RequestCloseEventHandler = () => void; export type RequestCloseEventHandler = () => void;
export type MapTokensStateCreateHandler = (states: TokenState[]) => void; export type MapTokensStateCreateHandler = (states: TokenState[]) => void;
export type MapTokenStateRemoveHandler = (state: TokenState) => void;
export type TokenStateChangeEventHandler = ( export type TokenStateChangeEventHandler = (
change: Partial<Record<string, Partial<TokenState>>> changes: Record<string, Partial<TokenState>>
) => void; ) => void;
export type TokenMenuOpenChangeEventHandler = ( export type TokenMenuOpenChangeEventHandler = (
tokenStateId: string, tokenStateId: string,
tokenImage: Konva.Node tokenImage: Konva.Node
) => void; ) => void;
export type NoteAddEventHander = (note: Note) => void;
export type NoteRemoveEventHander = (noteId: string) => void;
export type NoteChangeEventHandler = (change: Partial<Note>) => void;
export type NoteMenuOpenEventHandler = (
noteId: string,
noteNode: Konva.Node
) => void;
export type NoteDragEventHandler = (
event: Konva.KonvaEventObject<DragEvent>,
noteId: string
) => void;

View File

@ -1,7 +1,11 @@
import React from "react"; import React from "react";
import Action from "../actions/Action";
import { Drawing } from "./Drawing";
import { Fog } from "./Fog";
import { Grid } from "./Grid"; import { Grid } from "./Grid";
export type MapToolId = export type MapToolId =
| "map"
| "move" | "move"
| "fog" | "fog"
| "drawing" | "drawing"
@ -42,12 +46,30 @@ export type FileMapResolutions = {
ultra?: string; ultra?: string;
}; };
export type MapQuality = keyof FileMapResolutions | "original";
export type FileMap = BaseMap & { export type FileMap = BaseMap & {
type: "file"; type: "file";
file: string; file: string;
resolutions: FileMapResolutions; resolutions: FileMapResolutions;
thumbnail: string; thumbnail: string;
quality: keyof FileMapResolutions | "original"; quality: MapQuality;
}; };
export type Map = DefaultMap | FileMap; export type Map = DefaultMap | FileMap;
export type MapActions = {
mapDrawActions: Action<Drawing>[];
mapDrawActionIndex: number;
fogDrawActions: Action<Fog>[];
fogDrawActionIndex: number;
};
export type MapActionsKey = keyof Pick<
MapActions,
"mapDrawActions" | "fogDrawActions"
>;
export type MapActionsIndexKey = keyof Pick<
MapActions,
"mapDrawActionIndex" | "fogDrawActionIndex"
>;

View File

@ -1,3 +1,4 @@
import Konva from "konva";
import { Color } from "../helpers/colors"; import { Color } from "../helpers/colors";
export type Note = { export type Note = {
@ -13,3 +14,14 @@ export type Note = {
x: number; x: number;
y: number; y: number;
}; };
export type NoteMenuOptions = {
noteId: string;
noteNode: Konva.Node;
};
export type NoteDraggingOptions = {
dragging: boolean;
noteId: string;
noteGroup: Konva.Node;
};

View File

@ -1,4 +1,6 @@
import Konva from "konva";
import { Outline } from "./Outline"; import { Outline } from "./Outline";
import { TokenState } from "./TokenState";
export type TokenCategory = "character" | "vehicle" | "prop"; export type TokenCategory = "character" | "vehicle" | "prop";
@ -29,3 +31,14 @@ export type FileToken = BaseToken & {
}; };
export type Token = DefaultToken | FileToken; export type Token = DefaultToken | FileToken;
export type TokenMenuOptions = {
tokenStateId: string;
tokenImage: Konva.Node;
};
export type TokenDraggingOptions = {
dragging: boolean;
tokenState: TokenState;
tokenGroup: Konva.Node;
};

View File

@ -1,11 +1,12 @@
import { Outline } from "./Outline"; import { Outline } from "./Outline";
import { TokenCategory } from "./Token";
export type BaseTokenState = { export type BaseTokenState = {
id: string; id: string;
tokenId: string; tokenId: string;
owner: string; owner: string;
size: number; size: number;
category: string; category: TokenCategory;
label: string; label: string;
statuses: string[]; statuses: string[];
x: number; x: number;