diff --git a/src/components/Select.tsx b/src/components/Select.tsx index a412982..c358076 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -3,7 +3,7 @@ import Creatable from "react-select/creatable"; import { useThemeUI } from "theme-ui"; type SelectProps = { - creatable: boolean; + creatable?: boolean; } & Props; function Select({ creatable, ...props }: SelectProps) { @@ -76,4 +76,8 @@ function Select({ creatable, ...props }: SelectProps) { ); } +Select.defaultProps = { + creatable: false, +}; + export default Select; diff --git a/src/components/map/DragOverlay.tsx b/src/components/map/DragOverlay.tsx index cd831b6..c685e17 100644 --- a/src/components/map/DragOverlay.tsx +++ b/src/components/map/DragOverlay.tsx @@ -72,29 +72,31 @@ function DragOverlay({ dragging, node, onRemove }: DragOverlayProps) { } }); + if (!dragging) { + return null; + } + return ( - dragging && ( - - - - - - ) + + + + + ); } diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index e9c84e7..f4419ea 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { Box } from "theme-ui"; import { useToasts } from "react-toast-notifications"; @@ -27,15 +27,48 @@ import { RemoveStatesAction, } from "../../actions"; import Session from "../../network/Session"; -import { Drawing } from "../../types/Drawing"; -import { Fog } from "../../types/Fog"; -import { Map, MapToolId } from "../../types/Map"; +import { Drawing, DrawingState } from "../../types/Drawing"; +import { Fog, FogState } from "../../types/Fog"; +import { Map, MapActions, MapToolId } from "../../types/Map"; import { MapState } from "../../types/MapState"; import { Settings } from "../../types/Settings"; import { MapChangeEventHandler, MapResetEventHandler, + MapTokensStateCreateHandler, + MapTokenStateRemoveHandler, + NoteChangeEventHandler, + NoteRemoveEventHander, + TokenStateChangeEventHandler, } 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) => void; + onMapDrawUndo: () => void; + onMapDrawRedo: () => void; + onFogDraw: (action: Action) => void; + onFogDrawUndo: () => void; + onFogDrawRedo: () => void; + onMapNoteChange: NoteChangeEventHandler; + onMapNoteRemove: NoteRemoveEventHander; + allowMapDrawing: boolean; + allowFogDrawing: boolean; + allowMapChange: boolean; + allowNoteEditing: boolean; + disabledTokens: string[]; + session: Session; +}; function Map({ map, @@ -59,29 +92,7 @@ function Map({ allowNoteEditing, disabledTokens, session, -}: { - 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; -}) { +}: MapProps) { const { addToast } = useToasts(); const { tokensById } = useTokenData(); @@ -141,7 +152,7 @@ function Map({ onFogDraw(new EditStatesAction(shapes)); } - const disabledControls = []; + const disabledControls: MapToolId[] = []; if (!allowMapDrawing) { disabledControls.push("drawing"); } @@ -206,9 +217,10 @@ function Map({ ); const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); - const [tokenMenuOptions, setTokenMenuOptions] = useState({}); - const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); - function handleTokenMenuOpen(tokenStateId: string, tokenImage) { + const [tokenMenuOptions, setTokenMenuOptions] = useState(); + const [tokenDraggingOptions, setTokenDraggingOptions] = + useState(); + function handleTokenMenuOpen(tokenStateId: string, tokenImage: Konva.Node) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); } @@ -220,7 +232,7 @@ function Map({ tokenDraggingOptions={tokenDraggingOptions} setTokenDraggingOptions={setTokenDraggingOptions} onMapTokenStateChange={onMapTokenStateChange} - handleTokenMenuOpen={handleTokenMenuOpen} + onTokenMenuOpen={handleTokenMenuOpen} selectedToolId={selectedToolId} disabledTokens={disabledTokens} /> @@ -231,8 +243,12 @@ function Map({ isOpen={isTokenMenuOpen} onRequestClose={() => setIsTokenMenuOpen(false)} onTokenStateChange={onMapTokenStateChange} - tokenState={mapState && mapState.tokens[tokenMenuOptions.tokenStateId]} - tokenImage={tokenMenuOptions.tokenImage} + tokenState={ + tokenMenuOptions && + mapState && + mapState.tokens[tokenMenuOptions.tokenStateId] + } + tokenImage={tokenMenuOptions && tokenMenuOptions.tokenImage} map={map} /> ); @@ -241,7 +257,7 @@ function Map({ { onMapTokenStateRemove(state); - setTokenDraggingOptions(null); + setTokenDraggingOptions(undefined); }} onTokenStateChange={onMapTokenStateChange} tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState} @@ -291,14 +307,19 @@ function Map({ ); const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); - const [noteMenuOptions, setNoteMenuOptions] = useState({}); - const [noteDraggingOptions, setNoteDraggingOptions] = useState(); - function handleNoteMenuOpen(noteId: string, noteNode) { + const [noteMenuOptions, setNoteMenuOptions] = useState(); + const [noteDraggingOptions, setNoteDraggingOptions] = + useState(); + function handleNoteMenuOpen(noteId: string, noteNode: Konva.Node) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); } - function sortNotes(a, b, noteDraggingOptions) { + function sortNotes( + a: Note, + b: Note, + noteDraggingOptions?: NoteDraggingOptions + ) { if ( noteDraggingOptions && noteDraggingOptions.dragging && @@ -341,6 +362,7 @@ function Map({ setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) } onNoteDragEnd={() => + noteDraggingOptions && setNoteDraggingOptions({ ...noteDraggingOptions, dragging: false }) } fadeOnHover={selectedToolId === "drawing"} @@ -352,23 +374,25 @@ function Map({ isOpen={isNoteMenuOpen} onRequestClose={() => setIsNoteMenuOpen(false)} onNoteChange={onMapNoteChange} - note={mapState && mapState.notes[noteMenuOptions.noteId]} - noteNode={noteMenuOptions.noteNode} + note={ + noteMenuOptions && mapState && mapState.notes[noteMenuOptions.noteId] + } + noteNode={noteMenuOptions?.noteNode} map={map} /> ); - const noteDragOverlay = ( + const noteDragOverlay = noteDraggingOptions ? ( { onMapNoteRemove(noteId); - setNoteDraggingOptions(null); + setNoteDraggingOptions(undefined); }} /> - ); + ) : null; return ( diff --git a/src/components/map/MapEditor.tsx b/src/components/map/MapEditor.tsx index 167b8fc..ff1baf2 100644 --- a/src/components/map/MapEditor.tsx +++ b/src/components/map/MapEditor.tsx @@ -24,8 +24,7 @@ import MapGrid from "./MapGrid"; import MapGridEditor from "./MapGridEditor"; import { Map } from "../../types/Map"; import { GridInset } from "../../types/Grid"; - -type MapSettingsChangeEventHandler = (change: Partial) => void; +import { MapSettingsChangeEventHandler } from "../../types/Events"; type MapEditorProps = { map: Map; diff --git a/src/components/map/MapMenu.tsx b/src/components/map/MapMenu.tsx index a60409a..9ff3f7f 100644 --- a/src/components/map/MapMenu.tsx +++ b/src/components/map/MapMenu.tsx @@ -9,10 +9,10 @@ type MapMenuProps = { isOpen: boolean; onRequestClose: RequestCloseEventHandler; onModalContent: (instance: HTMLDivElement) => void; - top: number; - left: number; - bottom: number; - right: number; + top: number | string; + left: number | string; + bottom: number | string; + right: number | string; children: React.ReactNode; style: React.CSSProperties; excludeNode: Node | null; diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.tsx similarity index 61% rename from src/components/map/MapNotes.js rename to src/components/map/MapNotes.tsx index d28f364..b84dc55 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef } from "react"; import shortid from "shortid"; import { Group } from "react-konva"; +import Konva from "konva"; import { useInteractionEmitter } from "../../contexts/MapInteractionContext"; import { useMapStage } from "../../contexts/MapStageContext"; @@ -13,8 +14,30 @@ import useGridSnapping from "../../hooks/useGridSnapping"; 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; +type MapNoteProps = { + map: Map; + active: boolean; + onNoteAdd: NoteAddEventHander; + onNoteChange: NoteChangeEventHandler; + notes: NoteType[]; + onNoteMenuOpen: NoteMenuOpenEventHandler; + draggable: boolean; + onNoteDragStart: NoteDragEventHandler; + onNoteDragEnd: NoteDragEventHandler; + fadeOnHover: boolean; +}; + function MapNotes({ map, active, @@ -26,14 +49,14 @@ function MapNotes({ onNoteDragStart, onNoteDragEnd, fadeOnHover, -}) { +}: MapNoteProps) { const interactionEmitter = useInteractionEmitter(); const userId = useUserId(); const mapStageRef = useMapStage(); const [isBrushDown, setIsBrushDown] = useState(false); - const [noteData, setNoteData] = useState(null); + const [noteData, setNoteData] = useState(null); - const creatingNoteRef = useRef(); + const creatingNoteRef = useRef(null); const snapPositionToGrid = useGridSnapping(); @@ -44,8 +67,14 @@ function MapNotes({ const mapStage = mapStageRef.current; function getBrushPosition() { + if (!mapStage) { + return; + } const mapImage = mapStage.findOne("#mapImage"); let position = getRelativePointerPosition(mapImage); + if (!position) { + return; + } if (map.snapToGrid) { position = snapPositionToGrid(position); } @@ -57,6 +86,9 @@ function MapNotes({ function handleBrushDown() { const brushPosition = getBrushPosition(); + if (!brushPosition || !userId) { + return; + } setNoteData({ x: brushPosition.x, y: brushPosition.y, @@ -76,17 +108,25 @@ function MapNotes({ function handleBrushMove() { if (noteData) { const brushPosition = getBrushPosition(); - setNoteData((prev) => ({ - ...prev, - x: brushPosition.x, - y: brushPosition.y, - })); + if (!brushPosition) { + return; + } + setNoteData((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + x: brushPosition.x, + y: brushPosition.y, + }; + }); setIsBrushDown(true); } } function handleBrushUp() { - if (noteData) { + if (noteData && creatingNoteRef.current) { onNoteAdd(noteData); onNoteMenuOpen(noteData.id, creatingNoteRef.current); } @@ -94,14 +134,14 @@ function MapNotes({ setIsBrushDown(false); } - interactionEmitter.on("dragStart", handleBrushDown); - interactionEmitter.on("drag", handleBrushMove); - interactionEmitter.on("dragEnd", handleBrushUp); + interactionEmitter?.on("dragStart", handleBrushDown); + interactionEmitter?.on("drag", handleBrushMove); + interactionEmitter?.on("dragEnd", handleBrushUp); return () => { - interactionEmitter.off("dragStart", handleBrushDown); - interactionEmitter.off("drag", handleBrushMove); - interactionEmitter.off("dragEnd", handleBrushUp); + interactionEmitter?.off("dragStart", handleBrushDown); + interactionEmitter?.off("drag", handleBrushMove); + interactionEmitter?.off("dragEnd", handleBrushUp); }; }); @@ -121,9 +161,7 @@ function MapNotes({ /> ))} - {isBrushDown && noteData && ( - - )} + {isBrushDown && noteData && } ); diff --git a/src/components/map/MapPointer.js b/src/components/map/MapPointer.tsx similarity index 58% rename from src/components/map/MapPointer.js rename to src/components/map/MapPointer.tsx index e78ac33..c467350 100644 --- a/src/components/map/MapPointer.js +++ b/src/components/map/MapPointer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { Group } from "react-konva"; import { @@ -15,7 +15,17 @@ import { } from "../../helpers/konva"; 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({ active, @@ -25,7 +35,7 @@ function MapPointer({ onPointerUp, visible, color, -}) { +}: MapPointerProps) { const mapWidth = useMapWidth(); const mapHeight = useMapHeight(); const interactionEmitter = useInteractionEmitter(); @@ -40,30 +50,36 @@ function MapPointer({ const mapStage = mapStageRef.current; function getBrushPosition() { + if (!mapStage) { + return; + } const mapImage = mapStage.findOne("#mapImage"); return getRelativePointerPositionNormalized(mapImage); } function handleBrushDown() { - onPointerDown && onPointerDown(getBrushPosition()); + const brushPosition = getBrushPosition(); + brushPosition && onPointerDown?.(brushPosition); } function handleBrushMove() { - onPointerMove && visible && onPointerMove(getBrushPosition()); + const brushPosition = getBrushPosition(); + brushPosition && visible && onPointerMove?.(brushPosition); } function handleBrushUp() { - onPointerMove && onPointerUp(getBrushPosition()); + const brushPosition = getBrushPosition(); + brushPosition && onPointerUp?.(brushPosition); } - interactionEmitter.on("dragStart", handleBrushDown); - interactionEmitter.on("drag", handleBrushMove); - interactionEmitter.on("dragEnd", handleBrushUp); + interactionEmitter?.on("dragStart", handleBrushDown); + interactionEmitter?.on("drag", handleBrushMove); + interactionEmitter?.on("dragEnd", handleBrushUp); return () => { - interactionEmitter.off("dragStart", handleBrushDown); - interactionEmitter.off("drag", handleBrushMove); - interactionEmitter.off("dragEnd", handleBrushUp); + interactionEmitter?.off("dragStart", handleBrushDown); + interactionEmitter?.off("drag", handleBrushMove); + interactionEmitter?.off("dragEnd", handleBrushUp); }; }); diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.tsx similarity index 70% rename from src/components/map/MapSettings.js rename to src/components/map/MapSettings.tsx index 8f05c3b..f867792 100644 --- a/src/components/map/MapSettings.js +++ b/src/components/map/MapSettings.tsx @@ -9,8 +9,16 @@ import { mapSources as defaultMapSources } from "../../maps"; import Divider from "../Divider"; 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: "medium", label: "Medium" }, { value: "high", label: "High" }, @@ -18,42 +26,53 @@ const qualitySettings = [ { value: "original", label: "Original" }, ]; -const gridTypeSettings = [ +type GridTypeSetting = { value: GridType; label: string }; +const gridTypeSettings: GridTypeSetting[] = [ { value: "square", label: "Square" }, { value: "hexVertical", label: "Hex Vertical" }, { value: "hexHorizontal", label: "Hex Horizontal" }, ]; -const gridSquareMeasurementTypeSettings = [ +type GridMeasurementTypeSetting = { value: GridMeasurementType; label: string }; +const gridSquareMeasurementTypeSettings: GridMeasurementTypeSetting[] = [ { value: "chebyshev", label: "Chessboard (D&D 5e)" }, { value: "alternating", label: "Alternating Diagonal (D&D 3.5e)" }, { value: "euclidean", label: "Euclidean" }, { value: "manhattan", label: "Manhattan" }, ]; -const gridHexMeasurementTypeSettings = [ +const gridHexMeasurementTypeSettings: GridMeasurementTypeSetting[] = [ { value: "manhattan", label: "Manhattan" }, { value: "euclidean", label: "Euclidean" }, ]; +type MapSettingsProps = { + map: Map; + mapState: MapState; + onSettingsChange: MapSettingsChangeEventHandler; + onStateSettingsChange: MapStateSettingsChangeEventHandler; +}; + function MapSettings({ map, mapState, onSettingsChange, onStateSettingsChange, -}) { - function handleFlagChange(event, flag) { +}: MapSettingsProps) { + function handleFlagChange( + event: React.ChangeEvent, + flag: EditFlag + ) { if (event.target.checked) { - onStateSettingsChange("editFlags", [...mapState.editFlags, flag]); + onStateSettingsChange({ editFlags: [...mapState.editFlags, flag] }); } else { - onStateSettingsChange( - "editFlags", - mapState.editFlags.filter((f) => f !== flag) - ); + onStateSettingsChange({ + editFlags: mapState.editFlags.filter((f) => f !== flag), + }); } } - function handleGridSizeXChange(event) { + function handleGridSizeXChange(event: React.ChangeEvent) { const value = parseInt(event.target.value) || 0; let grid = { ...map.grid, @@ -63,10 +82,10 @@ function MapSettings({ }, }; grid.inset = getGridUpdatedInset(grid, map.width, map.height); - onSettingsChange("grid", grid); + onSettingsChange({ grid }); } - function handleGridSizeYChange(event) { + function handleGridSizeYChange(event: React.ChangeEvent) { const value = parseInt(event.target.value) || 0; let grid = { ...map.grid, @@ -76,12 +95,15 @@ function MapSettings({ }, }; 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; - let grid = { + let grid: Grid = { ...map.grid, type, measurement: { @@ -90,10 +112,15 @@ function MapSettings({ }, }; 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 = { ...map.grid, measurement: { @@ -101,10 +128,19 @@ function MapSettings({ 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 + ) { const grid = { ...map.grid, measurement: { @@ -112,7 +148,7 @@ function MapSettings({ scale: event.target.value, }, }; - onSettingsChange("grid", grid); + onSettingsChange({ grid }); } const mapURL = useDataURL(map, defaultMapSources); @@ -124,7 +160,7 @@ function MapSettings({ const blob = await response.blob(); let size = blob.size; size /= 1000000; // Bytes to Megabytes - setMapSize(size.toFixed(2)); + setMapSize(parseFloat(size.toFixed(2))); } else { setMapSize(0); } @@ -168,7 +204,7 @@ function MapSettings({ onSettingsChange("name", e.target.value)} + onChange={(e) => onSettingsChange({ name: e.target.value })} disabled={mapEmpty} my={1} /> @@ -185,10 +221,11 @@ function MapSettings({ isDisabled={mapEmpty} options={gridTypeSettings} value={ - !mapEmpty && - gridTypeSettings.find((s) => s.value === map.grid.type) + mapEmpty + ? undefined + : gridTypeSettings.find((s) => s.value === map.grid.type) } - onChange={handleGridTypeChange} + onChange={handleGridTypeChange as any} isSearchable={false} /> @@ -197,7 +234,9 @@ function MapSettings({ onSettingsChange("showGrid", e.target.checked)} + onChange={(e) => + onSettingsChange({ showGrid: e.target.checked }) + } /> Draw Grid @@ -206,7 +245,7 @@ function MapSettings({ checked={!mapEmpty && map.snapToGrid} disabled={mapEmpty} onChange={(e) => - onSettingsChange("snapToGrid", e.target.checked) + onSettingsChange({ snapToGrid: e.target.checked }) } /> Snap to Grid @@ -224,12 +263,13 @@ function MapSettings({ : gridHexMeasurementTypeSettings } value={ - !mapEmpty && - gridSquareMeasurementTypeSettings.find( - (s) => s.value === map.grid.measurement.type - ) + mapEmpty + ? undefined + : gridSquareMeasurementTypeSettings.find( + (s) => s.value === map.grid.measurement.type + ) } - onChange={handleGridMeasurementTypeChange} + onChange={handleGridMeasurementTypeChange as any} isSearchable={false} /> @@ -254,14 +294,17 @@ function MapSettings({