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";
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;

View File

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

View File

@ -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<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({
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<boolean>(false);
const [tokenMenuOptions, setTokenMenuOptions] = useState({});
const [tokenDraggingOptions, setTokenDraggingOptions] = useState();
function handleTokenMenuOpen(tokenStateId: string, tokenImage) {
const [tokenMenuOptions, setTokenMenuOptions] = useState<TokenMenuOptions>();
const [tokenDraggingOptions, setTokenDraggingOptions] =
useState<TokenDraggingOptions>();
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({
<TokenDragOverlay
onTokenStateRemove={(state) => {
onMapTokenStateRemove(state);
setTokenDraggingOptions(null);
setTokenDraggingOptions(undefined);
}}
onTokenStateChange={onMapTokenStateChange}
tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState}
@ -291,14 +307,19 @@ function Map({
);
const [isNoteMenuOpen, setIsNoteMenuOpen] = useState<boolean>(false);
const [noteMenuOptions, setNoteMenuOptions] = useState({});
const [noteDraggingOptions, setNoteDraggingOptions] = useState();
function handleNoteMenuOpen(noteId: string, noteNode) {
const [noteMenuOptions, setNoteMenuOptions] = useState<NoteMenuOptions>();
const [noteDraggingOptions, setNoteDraggingOptions] =
useState<NoteDraggingOptions>();
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 ? (
<NoteDragOverlay
dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)}
noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup}
noteId={noteDraggingOptions && noteDraggingOptions.noteId}
dragging={noteDraggingOptions.dragging}
noteGroup={noteDraggingOptions.noteGroup}
noteId={noteDraggingOptions.noteId}
onNoteRemove={(noteId) => {
onMapNoteRemove(noteId);
setNoteDraggingOptions(null);
setNoteDraggingOptions(undefined);
}}
/>
);
) : null;
return (
<Box sx={{ flexGrow: 1 }}>

View File

@ -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<Map>) => void;
import { MapSettingsChangeEventHandler } from "../../types/Events";
type MapEditorProps = {
map: Map;

View File

@ -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;

View File

@ -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<NoteType | null>(null);
const creatingNoteRef = useRef();
const creatingNoteRef = useRef<Konva.Group>(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({
/>
))}
<Group ref={creatingNoteRef}>
{isBrushDown && noteData && (
<Note note={noteData} map={map} draggable={false} />
)}
{isBrushDown && noteData && <Note note={noteData} map={map} />}
</Group>
</Group>
);

View File

@ -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);
};
});

View File

@ -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<HTMLInputElement>,
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<HTMLInputElement>) {
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<HTMLInputElement>) {
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<HTMLInputElement>
) {
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({
<Input
name="name"
value={(map && map.name) || ""}
onChange={(e) => 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}
/>
</Box>
@ -197,7 +234,9 @@ function MapSettings({
<Checkbox
checked={!mapEmpty && map.showGrid}
disabled={mapEmpty}
onChange={(e) => onSettingsChange("showGrid", e.target.checked)}
onChange={(e) =>
onSettingsChange({ showGrid: e.target.checked })
}
/>
Draw Grid
</Label>
@ -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}
/>
</Box>
@ -254,14 +294,17 @@ function MapSettings({
<Select
options={qualitySettings}
value={
!mapEmpty &&
qualitySettings.find((s) => s.value === map.quality)
mapEmpty
? undefined
: qualitySettings.find((s) => s.value === map.quality)
}
isDisabled={mapEmpty}
onChange={(option) => onSettingsChange("quality", option.value)}
isOptionDisabled={(option) =>
mapEmpty ||
(option.value !== "original" && !map.resolutions[option.value])
onChange={handleQualityChange as any}
isOptionDisabled={
((option: QualityTypeSetting) =>
mapEmpty ||
(option.value !== "original" &&
!map.resolutions[option.value])) as any
}
isSearchable={false}
/>

View File

@ -1,8 +1,19 @@
import React from "react";
import { Map } from "../../types/Map";
import Tile from "../tile/Tile";
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({
map,
isSelected,
@ -11,7 +22,7 @@ function MapTile({
onDoubleClick,
canEdit,
badges,
}) {
}: MapTileProps) {
return (
<Tile
title={map.name}

View File

@ -6,7 +6,24 @@ import MapImage from "./MapTileImage";
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();
return (

View File

@ -10,11 +10,26 @@ import { getGroupItems } from "../../helpers/group";
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 } =
useGroup();
function renderTile(group) {
function renderTile(group: Group) {
if (group.type === "item") {
const map = mapsById[group.id];
if (map) {

View File

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

View File

@ -1,19 +1,37 @@
import React from "react";
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";
type MapTokensProps = {
map: Map;
mapState: MapState;
tokenDraggingOptions?: TokenDraggingOptions;
setTokenDraggingOptions: (options: TokenDraggingOptions) => void;
onMapTokenStateChange: TokenStateChangeEventHandler;
onTokenMenuOpen: TokenMenuOpenChangeEventHandler;
selectedToolId: MapToolId;
disabledTokens: string[];
};
function MapTokens({
map,
mapState,
tokenDraggingOptions,
setTokenDraggingOptions,
onMapTokenStateChange,
handleTokenMenuOpen,
onTokenMenuOpen,
selectedToolId,
disabledTokens,
}) {
function getMapTokenCategoryWeight(category) {
}: MapTokensProps) {
function getMapTokenCategoryWeight(category: TokenCategory) {
switch (category) {
case "character":
return 0;
@ -27,7 +45,11 @@ function MapTokens({
}
// 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 (b.category !== a.category) {
const aWeight = getMapTokenCategoryWeight(a.category);
@ -62,7 +84,7 @@ function MapTokens({
key={tokenState.id}
tokenState={tokenState}
onTokenStateChange={onMapTokenStateChange}
onTokenMenuOpen={handleTokenMenuOpen}
onTokenMenuOpen={onTokenMenuOpen}
onTokenDragStart={(e) =>
setTokenDraggingOptions({
dragging: true,
@ -71,6 +93,7 @@ function MapTokens({
})
}
onTokenDragEnd={() =>
tokenDraggingOptions &&
setTokenDraggingOptions({
...tokenDraggingOptions,
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 Konva from "konva";
import { useSpring, animated } from "@react-spring/konva";
import { useUserId } from "../../contexts/UserIdContext";
@ -15,8 +16,27 @@ import colors from "../../helpers/colors";
import usePrevious from "../../hooks/usePrevious";
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;
type NoteProps = {
note: NoteType;
map: Map;
onNoteChange?: NoteChangeEventHandler;
onNoteMenuOpen?: NoteMenuOpenEventHandler;
draggable: boolean;
onNoteDragStart?: NoteDragEventHandler;
onNoteDragEnd?: NoteDragEventHandler;
fadeOnHover: boolean;
};
function Note({
note,
map,
@ -26,7 +46,7 @@ function Note({
onNoteDragStart,
onNoteDragEnd,
fadeOnHover,
}) {
}: NoteProps) {
const userId = useUserId();
const mapWidth = useMapWidth();
@ -45,11 +65,11 @@ function Note({
const snapPositionToGrid = useGridSnapping();
function handleDragStart(event) {
onNoteDragStart && onNoteDragStart(event, note.id);
function handleDragStart(event: Konva.KonvaEventObject<DragEvent>) {
onNoteDragStart?.(event, note.id);
}
function handleDragMove(event) {
function handleDragMove(event: Konva.KonvaEventObject<DragEvent>) {
const noteGroup = event.target;
// Snap to corners of grid
if (map.snapToGrid) {
@ -57,21 +77,20 @@ function Note({
}
}
function handleDragEnd(event) {
function handleDragEnd(event: Konva.KonvaEventObject<DragEvent>) {
const noteGroup = event.target;
onNoteChange &&
onNoteChange({
...note,
x: noteGroup.x() / mapWidth,
y: noteGroup.y() / mapHeight,
lastModifiedBy: userId,
lastModified: Date.now(),
});
onNoteDragEnd && onNoteDragEnd(note.id);
onNoteChange?.({
...note,
x: noteGroup.x() / mapWidth,
y: noteGroup.y() / mapHeight,
lastModifiedBy: userId,
lastModified: Date.now(),
});
onNoteDragEnd?.(event, note.id);
setPreventMapInteraction(false);
}
function handleClick(event) {
function handleClick(event: Konva.KonvaEventObject<MouseEvent>) {
if (draggable) {
const noteNode = event.target;
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
const notePointerDownTimeRef = useRef();
function handlePointerDown(event) {
const notePointerDownTimeRef = useRef<number>(0);
function handlePointerDown(event: Konva.KonvaEventObject<PointerEvent>) {
if (draggable) {
setPreventMapInteraction(true);
}
@ -89,7 +108,7 @@ function Note({
}
}
function handlePointerUp(event) {
function handlePointerUp(event: Konva.KonvaEventObject<PointerEvent>) {
if (draggable) {
setPreventMapInteraction(false);
}
@ -100,7 +119,7 @@ function Note({
const delta = event.evt.timeStamp - notePointerDownTimeRef.current;
if (delta < 300) {
const noteNode = event.target;
onNoteMenuOpen(note.id, noteNode);
onNoteMenuOpen?.(note.id, noteNode);
}
}
}
@ -121,12 +140,10 @@ function Note({
const [fontScale, setFontScale] = useState(1);
useEffect(() => {
const text = textRef.current;
if (!text) {
return;
}
function findFontSize() {
if (!text) {
return;
}
// Create an array from 1 / defaultFontSize of the note height to the full note height
let sizes = Array.from(
{ length: Math.ceil(noteHeight - notePadding * 2) },
@ -151,7 +168,7 @@ function Note({
findFontSize();
}, [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
const noteX = note.x * mapWidth;
@ -229,4 +246,9 @@ function Note({
);
}
Note.defaultProps = {
fadeOnHover: false,
draggable: false,
};
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 { Box, Flex, Text, IconButton } from "theme-ui";
import Konva from "konva";
import Slider from "../Slider";
import TextareaAutosize from "../TextareaAutoSize";
import MapMenu from "../map/MapMenu";
import colors, { colorOptions } from "../../helpers/colors";
import colors, { Color, colorOptions } from "../../helpers/colors";
import usePrevious from "../../hooks/usePrevious";
@ -19,8 +20,24 @@ import TextIcon from "../../icons/NoteTextIcon";
import { useUserId } from "../../contexts/UserIdContext";
import {
NoteChangeEventHandler,
RequestCloseEventHandler,
} from "../../types/Events";
import { Note } from "../../types/Note";
import { Map } from "../../types/Map";
const defaultNoteMaxSize = 6;
type NoteMenuProps = {
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
note?: Note;
noteNode?: Konva.Node;
onNoteChange: NoteChangeEventHandler;
map: Map;
};
function NoteMenu({
isOpen,
onRequestClose,
@ -28,7 +45,7 @@ function NoteMenu({
noteNode,
onNoteChange,
map,
}) {
}: NoteMenuProps) {
const userId = useUserId();
const wasOpen = usePrevious(isOpen);
@ -43,29 +60,30 @@ function NoteMenu({
if (noteNode) {
const nodeRect = noteNode.getClientRect();
const mapElement = document.querySelector(".map");
const mapRect = mapElement.getBoundingClientRect();
// Center X for the menu which is 156px wide
setMenuLeft(mapRect.left + nodeRect.x + nodeRect.width / 2 - 156 / 2);
// Y 12px from the bottom
setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 12);
if (mapElement) {
const mapRect = mapElement.getBoundingClientRect();
// Center X for the menu which is 156px wide
setMenuLeft(mapRect.left + nodeRect.x + nodeRect.width / 2 - 156 / 2);
// Y 12px from the bottom
setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 12);
}
}
}
}, [isOpen, note, wasOpen, noteNode]);
function handleTextChange(event) {
function handleTextChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
const text = event.target.value.substring(0, 1024);
note && onNoteChange({ ...note, text: text });
}
function handleColorChange(color) {
function handleColorChange(color: Color) {
if (!note) {
return;
}
onNoteChange({ ...note, color: color });
}
function handleSizeChange(event) {
function handleSizeChange(event: React.ChangeEvent<HTMLInputElement>) {
const newSize = parseFloat(event.target.value);
note && onNoteChange({ ...note, size: newSize });
}
@ -82,30 +100,35 @@ function NoteMenu({
note && onNoteChange({ ...note, textOnly: !note.textOnly });
}
function handleModalContent(node) {
function handleModalContent(node: HTMLElement) {
if (node) {
// Focus input
const tokenLabelInput = node.querySelector("#changeNoteText");
tokenLabelInput.focus();
tokenLabelInput.select();
const tokenLabelInput =
node.querySelector<HTMLInputElement>("#changeNoteText");
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)
);
}
}
}
function handleTextKeyPress(e) {
function handleTextKeyPress(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
onRequestClose();

View File

@ -23,33 +23,21 @@ import TokenBar from "../components/token/TokenBar";
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 {
Asset,
AssetManifest,
AssetManifestAsset,
AssetManifestAssets,
} from "../types/Asset";
import { TokenState } from "../types/TokenState";
import { Drawing, DrawingState } from "../types/Drawing";
import { Fog, 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"
>;
import { DrawingState } from "../types/Drawing";
import { FogState } from "../types/Fog";
const defaultMapActions: MapActions = {
mapDrawActions: [],

View File

@ -2,22 +2,39 @@ import Konva from "konva";
import { DefaultDice } from "./Dice";
import { Map } from "./Map";
import { MapState } from "./MapState";
import { Note } from "./Note";
import { TokenState } from "./TokenState";
export type MapChangeEventHandler = (map?: Map, mapState?: 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 RequestCloseEventHandler = () => void;
export type MapTokensStateCreateHandler = (states: TokenState[]) => void;
export type MapTokenStateRemoveHandler = (state: TokenState) => void;
export type TokenStateChangeEventHandler = (
change: Partial<Record<string, Partial<TokenState>>>
changes: Record<string, Partial<TokenState>>
) => void;
export type TokenMenuOpenChangeEventHandler = (
tokenStateId: string,
tokenImage: Konva.Node
) => 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 Action from "../actions/Action";
import { Drawing } from "./Drawing";
import { Fog } from "./Fog";
import { Grid } from "./Grid";
export type MapToolId =
| "map"
| "move"
| "fog"
| "drawing"
@ -42,12 +46,30 @@ export type FileMapResolutions = {
ultra?: string;
};
export type MapQuality = keyof FileMapResolutions | "original";
export type FileMap = BaseMap & {
type: "file";
file: string;
resolutions: FileMapResolutions;
thumbnail: string;
quality: keyof FileMapResolutions | "original";
quality: MapQuality;
};
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";
export type Note = {
@ -13,3 +14,14 @@ export type Note = {
x: 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 { TokenState } from "./TokenState";
export type TokenCategory = "character" | "vehicle" | "prop";
@ -29,3 +31,14 @@ export type FileToken = BaseToken & {
};
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 { TokenCategory } from "./Token";
export type BaseTokenState = {
id: string;
tokenId: string;
owner: string;
size: number;
category: string;
category: TokenCategory;
label: string;
statuses: string[];
x: number;