Added note menu and note text
This commit is contained in:
parent
927c596b04
commit
a3ae3471e8
@ -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 = (
|
||||
<MapNotes
|
||||
map={map}
|
||||
active={selectedToolId === "note"}
|
||||
gridSize={gridSizeNormalized}
|
||||
selectedToolSettings={settings[selectedToolId]}
|
||||
onNoteAdd={onMapNoteAdd}
|
||||
onNoteAdd={onMapNoteChange}
|
||||
onNoteChange={onMapNoteChange}
|
||||
// TODO: Sort by last modified
|
||||
notes={mapState ? Object.values(mapState.notes) : []}
|
||||
onNoteMenuOpen={handleNoteMenuOpen}
|
||||
/>
|
||||
);
|
||||
|
||||
const noteMenu = (
|
||||
<MapNoteMenu
|
||||
isOpen={isNoteMenuOpen}
|
||||
onRequestClose={() => 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}
|
||||
<MapLoadingOverlay />
|
||||
</>
|
||||
|
@ -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 (
|
||||
<Group>
|
||||
<Rect
|
||||
<Group
|
||||
onClick={handleClick}
|
||||
onTap={handleClick}
|
||||
x={note.x * mapWidth}
|
||||
y={note.y * mapHeight}
|
||||
width={noteWidth}
|
||||
height={noteHeight}
|
||||
offsetX={noteWidth / 2}
|
||||
offsetY={noteHeight / 2}
|
||||
fill="white"
|
||||
draggable={draggable}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragMove={handleDragMove}
|
||||
>
|
||||
<Rect
|
||||
width={noteWidth}
|
||||
height={noteHeight}
|
||||
shadowColor="rgba(0, 0, 0, 0.16)"
|
||||
shadowOffset={{ x: 0, y: 3 }}
|
||||
shadowBlur={6}
|
||||
cornerRadius={1}
|
||||
cornerRadius={0.25}
|
||||
fill={colors[note.color]}
|
||||
/>
|
||||
<Text
|
||||
text={note.text}
|
||||
fill="black"
|
||||
align="center"
|
||||
verticalAlign="middle"
|
||||
padding={textPadding}
|
||||
fontSize={fontSize}
|
||||
wrap="word"
|
||||
width={noteWidth}
|
||||
height={noteHeight}
|
||||
/>
|
||||
{/* Use an invisible text block to work out text sizing */}
|
||||
<Text visible={false} ref={textRef} text={note.text} wrap="none" />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
216
src/components/map/MapNoteMenu.js
Normal file
216
src/components/map/MapNoteMenu.js
Normal file
@ -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 (
|
||||
<MapMenu
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
top={`${menuTop}px`}
|
||||
left={`${menuLeft}px`}
|
||||
onModalContent={handleModalContent}
|
||||
>
|
||||
<Box sx={{ width: "156px" }} p={1}>
|
||||
<Flex
|
||||
as="form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onRequestClose();
|
||||
}}
|
||||
sx={{ alignItems: "center" }}
|
||||
>
|
||||
<Text
|
||||
as="label"
|
||||
variant="body2"
|
||||
sx={{ width: "45%", fontSize: "16px" }}
|
||||
p={1}
|
||||
>
|
||||
Label:
|
||||
</Text>
|
||||
<Input
|
||||
id="changeNoteText"
|
||||
onChange={handleTextChange}
|
||||
value={(note && note.text) || ""}
|
||||
sx={{
|
||||
padding: "4px",
|
||||
border: "none",
|
||||
":focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Flex>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{colorOptions.map((color) => (
|
||||
<Box
|
||||
key={color}
|
||||
sx={{
|
||||
width: "16.66%",
|
||||
paddingTop: "16.66%",
|
||||
borderRadius: "50%",
|
||||
transform: "scale(0.75)",
|
||||
backgroundColor: colors[color],
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => handleColorChange(color)}
|
||||
aria-label={`Note label Color ${color}`}
|
||||
>
|
||||
{note && note.color === color && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "2px solid white",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<Text
|
||||
as="label"
|
||||
variant="body2"
|
||||
sx={{ width: "40%", fontSize: "16px" }}
|
||||
p={1}
|
||||
>
|
||||
Size:
|
||||
</Text>
|
||||
<Slider
|
||||
value={(note && note.size) || 1}
|
||||
onChange={handleSizeChange}
|
||||
step={1}
|
||||
min={1}
|
||||
max={noteMaxSize}
|
||||
mr={1}
|
||||
/>
|
||||
</Flex>
|
||||
{/* Only show hide and lock token actions to map owners */}
|
||||
{map && map.owner === userId && (
|
||||
<Flex sx={{ alignItems: "center", justifyContent: "space-around" }}>
|
||||
<IconButton
|
||||
onClick={handleVisibleChange}
|
||||
title={note && note.visible ? "Hide Note" : "Show Note"}
|
||||
aria-label={note && note.visible ? "Hide Note" : "Show Note"}
|
||||
>
|
||||
{note && note.visible ? <ShowIcon /> : <HideIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleLockChange}
|
||||
title={note && note.locked ? "Unlock Note" : "Lock Note"}
|
||||
aria-label={note && note.locked ? "Unlock Note" : "Lock Note"}
|
||||
>
|
||||
{note && note.locked ? <LockIcon /> : <UnlockIcon />}
|
||||
</IconButton>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</MapMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapNoteMenu;
|
@ -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,6 +50,7 @@ function MapNotes({
|
||||
}
|
||||
|
||||
function handleBrushDown() {
|
||||
if (selectedToolSettings.type === "add") {
|
||||
const brushPosition = getBrushPosition();
|
||||
setNoteData({
|
||||
x: brushPosition.x,
|
||||
@ -57,11 +62,14 @@ function MapNotes({
|
||||
lastModifiedBy: userId,
|
||||
visible: true,
|
||||
locked: false,
|
||||
color: "yellow",
|
||||
});
|
||||
setIsBrushDown(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBrushMove() {
|
||||
if (selectedToolSettings.type === "add") {
|
||||
const brushPosition = getBrushPosition();
|
||||
setNoteData((prev) => ({
|
||||
...prev,
|
||||
@ -70,9 +78,13 @@ function MapNotes({
|
||||
}));
|
||||
setIsBrushDown(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBrushUp() {
|
||||
if (selectedToolSettings.type === "add") {
|
||||
onNoteAdd(noteData);
|
||||
onNoteMenuOpen(noteData.id, creatingNoteRef.current);
|
||||
}
|
||||
setNoteData(null);
|
||||
setIsBrushDown(false);
|
||||
}
|
||||
@ -91,9 +103,20 @@ function MapNotes({
|
||||
return (
|
||||
<Group>
|
||||
{notes.map((note) => (
|
||||
<MapNote note={note} map={map} key={note.id} />
|
||||
<MapNote
|
||||
note={note}
|
||||
map={map}
|
||||
key={note.id}
|
||||
onNoteMenuOpen={onNoteMenuOpen}
|
||||
draggable={active && selectedToolSettings.type === "move"}
|
||||
onNoteChange={onNoteChange}
|
||||
/>
|
||||
))}
|
||||
{isBrushDown && noteData && <MapNote note={noteData} map={map} />}
|
||||
<Group ref={creatingNoteRef}>
|
||||
{isBrushDown && noteData && (
|
||||
<MapNote note={noteData} map={map} draggable={false} />
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user