From a3ae3471e8ad113bea95a82ab6c634ec3ddf49af Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 4 Nov 2020 15:03:34 +1100 Subject: [PATCH] Added note menu and note text --- src/components/map/Map.js | 26 +++- src/components/map/MapNote.js | 130 ++++++++++++++-- src/components/map/MapNoteMenu.js | 216 +++++++++++++++++++++++++++ src/components/map/MapNotes.js | 71 ++++++--- src/network/NetworkedMapAndTokens.js | 8 +- 5 files changed, 411 insertions(+), 40 deletions(-) create mode 100644 src/components/map/MapNoteMenu.js diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 05f806d..710813f 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -19,6 +19,7 @@ import TokenMenu from "../token/TokenMenu"; import TokenDragOverlay from "../token/TokenDragOverlay"; import { drawActionsToShapes } from "../../helpers/drawing"; +import MapNoteMenu from "./MapNoteMenu"; function Map({ map, @@ -33,7 +34,7 @@ function Map({ onFogDraw, onFogDrawUndo, onFogDrawRedo, - onMapNoteAdd, + onMapNoteChange, allowMapDrawing, allowFogDrawing, allowMapChange, @@ -351,15 +352,35 @@ function Map({ /> ); + const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); + const [noteMenuOptions, setNoteMenuOptions] = useState({}); + function handleNoteMenuOpen(noteId, noteNode) { + setNoteMenuOptions({ noteId, noteNode }); + setIsNoteMenuOpen(true); + } + const mapNotes = ( + ); + + const noteMenu = ( + setIsNoteMenuOpen(false)} + onNoteChange={onMapNoteChange} + note={mapState && mapState.notes[noteMenuOptions.noteId]} + noteNode={noteMenuOptions.noteNode} + map={map} /> ); @@ -370,6 +391,7 @@ function Map({ <> {mapControls} {tokenMenu} + {noteMenu} {tokenDragOverlay} diff --git a/src/components/map/MapNote.js b/src/components/map/MapNote.js index bd95179..b2f5fba 100644 --- a/src/components/map/MapNote.js +++ b/src/components/map/MapNote.js @@ -1,29 +1,139 @@ -import React, { useContext } from "react"; -import { Group, Rect } from "react-konva"; +import React, { useContext, useEffect, useState, useRef } from "react"; +import { Group, Rect, Text } from "react-konva"; +import AuthContext from "../../contexts/AuthContext"; import MapInteractionContext from "../../contexts/MapInteractionContext"; -function MapNote({ note, map }) { +import * as Vector2 from "../../helpers/vector2"; +import colors from "../../helpers/colors"; + +const snappingThreshold = 1 / 5; +const textPadding = 4; + +function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { + const { userId } = useContext(AuthContext); const { mapWidth, mapHeight } = useContext(MapInteractionContext); const noteWidth = map && (mapWidth / map.grid.size.x) * note.size; const noteHeight = map && (mapHeight / map.grid.size.y) * note.size; + function handleClick(event) { + if (draggable) { + const noteNode = event.target; + onNoteMenuOpen && onNoteMenuOpen(note.id, noteNode); + } + } + + function handleDragMove(event) { + const noteGroup = event.target; + // Snap to corners of grid + if (map.snapToGrid) { + const offset = Vector2.multiply(map.grid.inset.topLeft, { + x: mapWidth, + y: mapHeight, + }); + const position = { + x: noteGroup.x() + noteGroup.width() / 2, + y: noteGroup.y() + noteGroup.height() / 2, + }; + const gridSize = { + x: + (mapWidth * + (map.grid.inset.bottomRight.x - map.grid.inset.topLeft.x)) / + map.grid.size.x, + y: + (mapHeight * + (map.grid.inset.bottomRight.y - map.grid.inset.topLeft.y)) / + map.grid.size.y, + }; + // Transform into offset space, round, then transform back + const gridSnap = Vector2.add( + Vector2.roundTo(Vector2.subtract(position, offset), gridSize), + offset + ); + const gridDistance = Vector2.length(Vector2.subtract(gridSnap, position)); + const minGrid = Vector2.min(gridSize); + if (gridDistance < minGrid * snappingThreshold) { + noteGroup.x(gridSnap.x - noteGroup.width() / 2); + noteGroup.y(gridSnap.y - noteGroup.height() / 2); + } + } + } + + function handleDragEnd(event) { + const noteGroup = event.target; + onNoteChange && + onNoteChange({ + ...note, + x: noteGroup.x() / mapWidth, + y: noteGroup.y() / mapHeight, + lastModifiedBy: userId, + lastModified: Date.now(), + }); + } + + const [fontSize, setFontSize] = useState(1); + useEffect(() => { + const text = textRef.current; + function findFontSize() { + // Create an array from 4 to 18 scaled to the note size + const sizes = Array.from( + { length: 14 * note.size }, + (_, i) => i + 4 * note.size + ); + + return sizes.reduce((prev, curr) => { + text.fontSize(curr); + const width = text.getTextWidth() + textPadding * 2; + if (width < noteWidth) { + return curr; + } else { + return prev; + } + }); + } + + setFontSize(findFontSize()); + }, [note, noteWidth]); + + const textRef = useRef(); + return ( - + + + {/* Use an invisible text block to work out text sizing */} + ); } diff --git a/src/components/map/MapNoteMenu.js b/src/components/map/MapNoteMenu.js new file mode 100644 index 0000000..d02afec --- /dev/null +++ b/src/components/map/MapNoteMenu.js @@ -0,0 +1,216 @@ +import React, { useEffect, useState, useContext } from "react"; +import { Box, Input, Slider, Flex, Text, IconButton } from "theme-ui"; + +import MapMenu from "../map/MapMenu"; + +import colors, { colorOptions } from "../../helpers/colors"; + +import usePrevious from "../../helpers/usePrevious"; + +import LockIcon from "../../icons/TokenLockIcon"; +import UnlockIcon from "../../icons/TokenUnlockIcon"; +import ShowIcon from "../../icons/TokenShowIcon"; +import HideIcon from "../../icons/TokenHideIcon"; + +import AuthContext from "../../contexts/AuthContext"; + +const defaultNoteMaxSize = 6; + +function MapNoteMenu({ + isOpen, + onRequestClose, + note, + noteNode, + onNoteChange, + map, +}) { + const { userId } = useContext(AuthContext); + + const wasOpen = usePrevious(isOpen); + + const [noteMaxSize, setNoteMaxSize] = useState(defaultNoteMaxSize); + const [menuLeft, setMenuLeft] = useState(0); + const [menuTop, setMenuTop] = useState(0); + useEffect(() => { + if (isOpen && !wasOpen && note) { + setNoteMaxSize(Math.max(note.size, defaultNoteMaxSize)); + // Update menu position + 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); + } + } + }, [isOpen, note, wasOpen, noteNode]); + + function handleTextChange(event) { + const text = event.target.value; + note && onNoteChange({ ...note, text: text }); + } + + function handleColorChange(color) { + if (!note) { + return; + } + onNoteChange({ ...note, color: color }); + } + + function handleSizeChange(event) { + const newSize = parseInt(event.target.value); + note && onNoteChange({ ...note, size: newSize }); + } + + function handleVisibleChange() { + note && onNoteChange({ ...note, visible: !note.visible }); + } + + function handleLockChange() { + note && onNoteChange({ ...note, locked: !note.locked }); + } + + function handleModalContent(node) { + if (node) { + // Focus input + const tokenLabelInput = node.querySelector("#changeNoteText"); + 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) + ); + } + } + + return ( + + + { + e.preventDefault(); + onRequestClose(); + }} + sx={{ alignItems: "center" }} + > + + Label: + + + + + {colorOptions.map((color) => ( + handleColorChange(color)} + aria-label={`Note label Color ${color}`} + > + {note && note.color === color && ( + + )} + + ))} + + + + Size: + + + + {/* Only show hide and lock token actions to map owners */} + {map && map.owner === userId && ( + + + {note && note.visible ? : } + + + {note && note.locked ? : } + + + )} + + + ); +} + +export default MapNoteMenu; diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js index 64a0463..64b1591 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.js @@ -1,4 +1,4 @@ -import React, { useContext, useState, useEffect } from "react"; +import React, { useContext, useState, useEffect, useRef } from "react"; import shortid from "shortid"; import { Group } from "react-konva"; @@ -19,7 +19,9 @@ function MapNotes({ active, gridSize, onNoteAdd, + onNoteChange, notes, + onNoteMenuOpen, }) { const { interactionEmitter } = useContext(MapInteractionContext); const { userId } = useContext(AuthContext); @@ -27,6 +29,8 @@ function MapNotes({ const [isBrushDown, setIsBrushDown] = useState(false); const [noteData, setNoteData] = useState(null); + const creatingNoteRef = useRef(); + useEffect(() => { if (!active) { return; @@ -46,33 +50,41 @@ function MapNotes({ } function handleBrushDown() { - const brushPosition = getBrushPosition(); - setNoteData({ - x: brushPosition.x, - y: brushPosition.y, - size: defaultNoteSize, - text: "", - id: shortid.generate(), - lastModified: Date.now(), - lastModifiedBy: userId, - visible: true, - locked: false, - }); - setIsBrushDown(true); + if (selectedToolSettings.type === "add") { + const brushPosition = getBrushPosition(); + setNoteData({ + x: brushPosition.x, + y: brushPosition.y, + size: defaultNoteSize, + text: "", + id: shortid.generate(), + lastModified: Date.now(), + lastModifiedBy: userId, + visible: true, + locked: false, + color: "yellow", + }); + setIsBrushDown(true); + } } function handleBrushMove() { - const brushPosition = getBrushPosition(); - setNoteData((prev) => ({ - ...prev, - x: brushPosition.x, - y: brushPosition.y, - })); - setIsBrushDown(true); + if (selectedToolSettings.type === "add") { + const brushPosition = getBrushPosition(); + setNoteData((prev) => ({ + ...prev, + x: brushPosition.x, + y: brushPosition.y, + })); + setIsBrushDown(true); + } } function handleBrushUp() { - onNoteAdd(noteData); + if (selectedToolSettings.type === "add") { + onNoteAdd(noteData); + onNoteMenuOpen(noteData.id, creatingNoteRef.current); + } setNoteData(null); setIsBrushDown(false); } @@ -91,9 +103,20 @@ function MapNotes({ return ( {notes.map((note) => ( - + ))} - {isBrushDown && noteData && } + + {isBrushDown && noteData && ( + + )} + ); } diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 98c992a..6090a72 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -167,7 +167,7 @@ function NetworkedMapAndTokens({ session }) { session.send("mapFogIndex", index); } - function handleNoteAdd(note) { + function handleNoteChange(note) { setCurrentMapState((prevMapState) => ({ ...prevMapState, notes: { @@ -175,7 +175,7 @@ function NetworkedMapAndTokens({ session }) { [note.id]: note, }, })); - session.send("mapNoteAdd", note); + session.send("mapNoteChange", note); } /** @@ -406,7 +406,7 @@ function NetworkedMapAndTokens({ session }) { fogDrawActionIndex: data, })); } - if (id === "mapNoteAdd" && currentMapState) { + if (id === "mapNoteChange" && currentMapState) { setCurrentMapState((prevMapState) => ({ ...prevMapState, notes: { @@ -480,7 +480,7 @@ function NetworkedMapAndTokens({ session }) { onFogDraw={handleFogDraw} onFogDrawUndo={handleFogDrawUndo} onFogDrawRedo={handleFogDrawRedo} - onMapNoteAdd={handleNoteAdd} + onMapNoteChange={handleNoteChange} allowMapDrawing={canEditMapDrawing} allowFogDrawing={canEditFogDrawing} allowMapChange={canChangeMap}