diff --git a/src/components/DragOverlay.js b/src/components/DragOverlay.js new file mode 100644 index 0000000..18678e7 --- /dev/null +++ b/src/components/DragOverlay.js @@ -0,0 +1,86 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Box, IconButton } from "theme-ui"; + +import RemoveTokenIcon from "../icons/RemoveTokenIcon"; + +function DragOverlay({ dragging, node, onRemove }) { + const [isRemoveHovered, setIsRemoveHovered] = useState(false); + const removeTokenRef = useRef(); + + // Detect token hover on remove icon manually to support touch devices + useEffect(() => { + const map = document.querySelector(".map"); + const mapRect = map.getBoundingClientRect(); + + function detectRemoveHover() { + if (!node || !dragging || !removeTokenRef.current) { + return; + } + + const pointerPosition = node.getStage().getPointerPosition(); + const screenSpacePointerPosition = { + x: pointerPosition.x + mapRect.left, + y: pointerPosition.y + mapRect.top, + }; + const removeIconPosition = removeTokenRef.current.getBoundingClientRect(); + + if ( + screenSpacePointerPosition.x > removeIconPosition.left && + screenSpacePointerPosition.y > removeIconPosition.top && + screenSpacePointerPosition.x < removeIconPosition.right && + screenSpacePointerPosition.y < removeIconPosition.bottom + ) { + if (!isRemoveHovered) { + setIsRemoveHovered(true); + } + } else if (isRemoveHovered) { + setIsRemoveHovered(false); + } + } + + let handler; + if (node && dragging) { + handler = setInterval(detectRemoveHover, 100); + } + + return () => { + if (handler) { + clearInterval(handler); + } + }; + }, [isRemoveHovered, dragging, node]); + + // Detect drag end of token image and remove it if it is over the remove icon + useEffect(() => { + if (!dragging && node && isRemoveHovered) { + onRemove(); + } + }); + + return ( + dragging && ( + + + + + + ) + ); +} + +export default DragOverlay; diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 0bca52e..29467db 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -17,9 +17,10 @@ import SettingsContext from "../../contexts/SettingsContext"; import TokenMenu from "../token/TokenMenu"; import TokenDragOverlay from "../token/TokenDragOverlay"; +import NoteMenu from "../note/NoteMenu"; +import NoteDragOverlay from "../note/NoteDragOverlay"; import { drawActionsToShapes } from "../../helpers/drawing"; -import MapNoteMenu from "./MapNoteMenu"; function Map({ map, @@ -35,6 +36,7 @@ function Map({ onFogDrawUndo, onFogDrawRedo, onMapNoteChange, + onMapNoteRemove, allowMapDrawing, allowFogDrawing, allowMapChange, @@ -186,7 +188,7 @@ function Map({ const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); const [tokenMenuOptions, setTokenMenuOptions] = useState({}); - const [draggingTokenOptions, setDraggingTokenOptions] = useState(); + const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); function handleTokenMenuOpen(tokenStateId, tokenImage) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); @@ -245,7 +247,7 @@ function Map({ const mapTokens = map && mapState && ( {Object.values(mapState.tokens) - .sort((a, b) => sortMapTokenStates(a, b, draggingTokenOptions)) + .sort((a, b) => sortMapTokenStates(a, b, tokenDraggingOptions)) .map((tokenState) => ( - setDraggingTokenOptions({ + setTokenDraggingOptions({ dragging: true, tokenState, tokenGroup: e.target, }) } onTokenDragEnd={() => - setDraggingTokenOptions({ - ...draggingTokenOptions, + setTokenDraggingOptions({ + ...tokenDraggingOptions, dragging: false, }) } @@ -291,17 +293,17 @@ function Map({ /> ); - const tokenDragOverlay = draggingTokenOptions && ( + const tokenDragOverlay = tokenDraggingOptions && ( { onMapTokenStateRemove(state); - setDraggingTokenOptions(null); + setTokenDraggingOptions(null); }} onTokenStateChange={onMapTokenStateChange} - tokenState={draggingTokenOptions && draggingTokenOptions.tokenState} - tokenGroup={draggingTokenOptions && draggingTokenOptions.tokenGroup} - dragging={draggingTokenOptions && draggingTokenOptions.dragging} - token={tokensById[draggingTokenOptions.tokenState.tokenId]} + tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState} + tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup} + dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)} + token={tokensById[tokenDraggingOptions.tokenState.tokenId]} mapState={mapState} /> ); @@ -354,6 +356,7 @@ function Map({ const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); const [noteMenuOptions, setNoteMenuOptions] = useState({}); + const [noteDraggingOptions, setNoteDraggingOptions] = useState(); function handleNoteMenuOpen(noteId, noteNode) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); @@ -371,11 +374,17 @@ function Map({ notes={mapState ? Object.values(mapState.notes) : []} onNoteMenuOpen={handleNoteMenuOpen} draggable={selectedToolId === "note" || selectedToolId === "pan"} + onNoteDragStart={(e, noteId) => + setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) + } + onNoteDragEnd={() => + setNoteDraggingOptions({ ...noteDraggingOptions, dragging: false }) + } /> ); const noteMenu = ( - setIsNoteMenuOpen(false)} onNoteChange={onMapNoteChange} @@ -385,6 +394,18 @@ function Map({ /> ); + const noteDragOverlay = ( + { + onMapNoteRemove(noteId); + setNoteDraggingOptions(null); + }} + /> + ); + return ( } diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js index d297cb3..fb81e21 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.js @@ -9,7 +9,7 @@ import AuthContext from "../../contexts/AuthContext"; import { getBrushPositionForTool } from "../../helpers/drawing"; import { getRelativePointerPositionNormalized } from "../../helpers/konva"; -import MapNote from "./MapNote"; +import Note from "../note/Note"; const defaultNoteSize = 2; @@ -22,6 +22,8 @@ function MapNotes({ notes, onNoteMenuOpen, draggable, + onNoteDragStart, + onNoteDragEnd, }) { const { interactionEmitter } = useContext(MapInteractionContext); const { userId } = useContext(AuthContext); @@ -99,18 +101,20 @@ function MapNotes({ return ( {notes.map((note) => ( - ))} {isBrushDown && noteData && ( - + )} diff --git a/src/components/map/MapNote.js b/src/components/note/Note.js similarity index 91% rename from src/components/map/MapNote.js rename to src/components/note/Note.js index 0afea46..2e7de59 100644 --- a/src/components/map/MapNote.js +++ b/src/components/note/Note.js @@ -10,7 +10,15 @@ import colors from "../../helpers/colors"; const snappingThreshold = 1 / 5; const textPadding = 4; -function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { +function Note({ + note, + map, + onNoteChange, + onNoteMenuOpen, + draggable, + onNoteDragStart, + onNoteDragEnd, +}) { const { userId } = useContext(AuthContext); const { mapWidth, mapHeight, setPreventMapInteraction } = useContext( MapInteractionContext @@ -26,6 +34,10 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { } } + function handleDragStart(event) { + onNoteDragStart && onNoteDragStart(event, note.id); + } + function handleDragMove(event) { const noteGroup = event.target; // Snap to corners of grid @@ -72,15 +84,17 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { lastModifiedBy: userId, lastModified: Date.now(), }); + onNoteDragEnd && onNoteDragEnd(note.id); + setPreventMapInteraction(false); } - function handlePointerDown(event) { + function handlePointerDown() { if (draggable) { setPreventMapInteraction(true); } } - function handlePointerUp(event) { + function handlePointerUp() { if (draggable) { setPreventMapInteraction(false); } @@ -123,6 +137,7 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { offsetX={noteWidth / 2} offsetY={noteHeight / 2} draggable={draggable} + onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragMove={handleDragMove} onMouseDown={handlePointerDown} @@ -157,4 +172,4 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { ); } -export default MapNote; +export default Note; diff --git a/src/components/note/NoteDragOverlay.js b/src/components/note/NoteDragOverlay.js new file mode 100644 index 0000000..9df0713 --- /dev/null +++ b/src/components/note/NoteDragOverlay.js @@ -0,0 +1,19 @@ +import React from "react"; + +import DragOverlay from "../DragOverlay"; + +function NoteDragOverlay({ onNoteRemove, noteId, noteGroup, dragging }) { + function handleNoteRemove() { + onNoteRemove(noteId); + } + + return ( + + ); +} + +export default NoteDragOverlay; diff --git a/src/components/map/MapNoteMenu.js b/src/components/note/NoteMenu.js similarity index 99% rename from src/components/map/MapNoteMenu.js rename to src/components/note/NoteMenu.js index 7781b92..783025d 100644 --- a/src/components/map/MapNoteMenu.js +++ b/src/components/note/NoteMenu.js @@ -16,7 +16,7 @@ import AuthContext from "../../contexts/AuthContext"; const defaultNoteMaxSize = 6; -function MapNoteMenu({ +function NoteMenu({ isOpen, onRequestClose, note, @@ -205,4 +205,4 @@ function MapNoteMenu({ ); } -export default MapNoteMenu; +export default NoteMenu; diff --git a/src/components/token/TokenDragOverlay.js b/src/components/token/TokenDragOverlay.js index 68b9d29..4d90e9a 100644 --- a/src/components/token/TokenDragOverlay.js +++ b/src/components/token/TokenDragOverlay.js @@ -1,11 +1,10 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; -import { Box, IconButton } from "theme-ui"; - -import RemoveTokenIcon from "../../icons/RemoveTokenIcon"; +import React, { useContext } from "react"; import AuthContext from "../../contexts/AuthContext"; import MapInteractionContext from "../../contexts/MapInteractionContext"; +import DragOverlay from "../DragOverlay"; + function TokenDragOverlay({ onTokenStateRemove, onTokenStateChange, @@ -16,114 +15,38 @@ function TokenDragOverlay({ mapState, }) { const { userId } = useContext(AuthContext); - const { setPreventMapInteraction, mapWidth, mapHeight } = useContext( - MapInteractionContext - ); + const { mapWidth, mapHeight } = useContext(MapInteractionContext); - const [isRemoveHovered, setIsRemoveHovered] = useState(false); - const removeTokenRef = useRef(); - - // Detect token hover on remove icon manually to support touch devices - useEffect(() => { - const map = document.querySelector(".map"); - const mapRect = map.getBoundingClientRect(); - - function detectRemoveHover() { - if (!tokenGroup) { - return; - } - - const pointerPosition = tokenGroup.getStage().getPointerPosition(); - const screenSpacePointerPosition = { - x: pointerPosition.x + mapRect.left, - y: pointerPosition.y + mapRect.top, - }; - if (!removeTokenRef.current) { - return; - } - const removeIconPosition = removeTokenRef.current.getBoundingClientRect(); - - if ( - screenSpacePointerPosition.x > removeIconPosition.left && - screenSpacePointerPosition.y > removeIconPosition.top && - screenSpacePointerPosition.x < removeIconPosition.right && - screenSpacePointerPosition.y < removeIconPosition.bottom - ) { - if (!isRemoveHovered) { - setIsRemoveHovered(true); - } - } else if (isRemoveHovered) { - setIsRemoveHovered(false); + function handleTokenRemove() { + // Handle other tokens when a vehicle gets deleted + if (token && token.category === "vehicle") { + const layer = tokenGroup.getLayer(); + const mountedTokens = tokenGroup.find(".token"); + for (let mountedToken of mountedTokens) { + // Save and restore token position after moving layer + const position = mountedToken.absolutePosition(); + mountedToken.moveTo(layer); + mountedToken.absolutePosition(position); + onTokenStateChange({ + [mountedToken.id()]: { + ...mapState.tokens[mountedToken.id()], + x: mountedToken.x() / mapWidth, + y: mountedToken.y() / mapHeight, + lastModifiedBy: userId, + lastModified: Date.now(), + }, + }); } } - - let handler; - if (tokenState && tokenGroup && dragging) { - handler = setInterval(detectRemoveHover, 100); - } - - return () => { - if (handler) { - clearInterval(handler); - } - }; - }, [tokenState, tokenGroup, isRemoveHovered, dragging]); - - // Detect drag end of token image and remove it if it is over the remove icon - useEffect(() => { - function handleTokenDragEnd() { - // Handle other tokens when a vehicle gets deleted - if (token && token.category === "vehicle") { - const layer = tokenGroup.getLayer(); - const mountedTokens = tokenGroup.find(".token"); - for (let mountedToken of mountedTokens) { - // Save and restore token position after moving layer - const position = mountedToken.absolutePosition(); - mountedToken.moveTo(layer); - mountedToken.absolutePosition(position); - onTokenStateChange({ - [mountedToken.id()]: { - ...mapState.tokens[mountedToken.id()], - x: mountedToken.x() / mapWidth, - y: mountedToken.y() / mapHeight, - lastModifiedBy: userId, - lastModified: Date.now(), - }, - }); - } - } - onTokenStateRemove(tokenState); - setPreventMapInteraction(false); - } - - if (!dragging && tokenState && isRemoveHovered) { - handleTokenDragEnd(); - } - }); + onTokenStateRemove(tokenState); + } return ( - dragging && ( - - - - - - ) + ); } diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 6090a72..9d22be7 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -178,6 +178,14 @@ function NetworkedMapAndTokens({ session }) { session.send("mapNoteChange", note); } + function handleNoteRemove(noteId) { + setCurrentMapState((prevMapState) => ({ + ...prevMapState, + notes: omit(prevMapState.notes, [noteId]), + })); + session.send("mapNoteRemove", noteId); + } + /** * Token state */ @@ -415,6 +423,12 @@ function NetworkedMapAndTokens({ session }) { }, })); } + if (id === "mapNoteRemove" && currentMapState) { + setCurrentMapState((prevMapState) => ({ + ...prevMapState, + notes: omit(prevMapState.notes, [data]), + })); + } } function handlePeerDataProgress({ id, total, count }) { @@ -481,6 +495,7 @@ function NetworkedMapAndTokens({ session }) { onFogDrawUndo={handleFogDrawUndo} onFogDrawRedo={handleFogDrawRedo} onMapNoteChange={handleNoteChange} + onMapNoteRemove={handleNoteRemove} allowMapDrawing={canEditMapDrawing} allowFogDrawing={canEditFogDrawing} allowMapChange={canChangeMap}