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}