diff --git a/src/components/konva/Note.tsx b/src/components/konva/Note.tsx index d23972c..9176ade 100644 --- a/src/components/konva/Note.tsx +++ b/src/components/konva/Note.tsx @@ -20,10 +20,13 @@ import { Note as NoteType } from "../../types/Note"; import { NoteChangeEventHandler, NoteDragEventHandler, + NoteMenuCloseEventHandler, NoteMenuOpenEventHandler, } from "../../types/Events"; import { Map } from "../../types/Map"; +import Transformer from "./Transformer"; + const defaultFontSize = 16; type NoteProps = { @@ -31,10 +34,12 @@ type NoteProps = { map: Map | null; onNoteChange?: NoteChangeEventHandler; onNoteMenuOpen?: NoteMenuOpenEventHandler; + onNoteMenuClose?: NoteMenuCloseEventHandler; draggable: boolean; onNoteDragStart?: NoteDragEventHandler; onNoteDragEnd?: NoteDragEventHandler; fadeOnHover: boolean; + selected: boolean; }; function Note({ @@ -42,10 +47,12 @@ function Note({ map, onNoteChange, onNoteMenuOpen, + onNoteMenuClose, draggable, onNoteDragStart, onNoteDragEnd, fadeOnHover, + selected, }: NoteProps) { const userId = useUserId(); @@ -173,6 +180,30 @@ function Note({ const textRef = useRef(null); + const noteRef = useRef(null); + const [isTransforming, setIsTransforming] = useState(false); + function handleTransformStart() { + setIsTransforming(true); + onNoteMenuClose?.(); + } + + function handleTransformEnd(event: Konva.KonvaEventObject) { + if (noteRef.current) { + const sizeChange = event.target.scaleX(); + const rotation = event.target.rotation(); + onNoteChange?.({ + [note.id]: { + size: note.size * sizeChange, + rotation: rotation, + }, + }); + onNoteMenuOpen?.(note.id, noteRef.current); + noteRef.current.scaleX(1); + noteRef.current.scaleY(1); + } + setIsTransforming(false); + } + // Animate to new note positions if edited by others const noteX = note.x * mapWidth; const noteY = note.y * mapHeight; @@ -194,62 +225,73 @@ function Note({ const noteName = `note${note.locked ? "-locked" : ""}`; return ( - - {!note.textOnly && ( - + + {!note.textOnly && ( + + )} + - )} - + + - {/* Use an invisible text block to work out text sizing */} - - + ); } diff --git a/src/components/konva/Transformer.tsx b/src/components/konva/Transformer.tsx index 181d540..1790523 100644 --- a/src/components/konva/Transformer.tsx +++ b/src/components/konva/Transformer.tsx @@ -148,7 +148,7 @@ function Transformer({ if (movingAnchor === "rotater") { text.text(`${node.rotation().toFixed(0)}°`); } else { - const nodeRect = node.getClientRect(); + const nodeRect = node.getClientRect({ skipShadow: true }); const nodeScale = Vector2.divide( { x: nodeRect.width, y: nodeRect.height }, gridCellAbsoluteSize diff --git a/src/components/note/NoteMenu.tsx b/src/components/note/NoteMenu.tsx index 9f66b37..2ba01c1 100644 --- a/src/components/note/NoteMenu.tsx +++ b/src/components/note/NoteMenu.tsx @@ -1,8 +1,7 @@ import React, { useEffect, useState } from "react"; -import { Box, Flex, Text, IconButton } from "theme-ui"; +import { Box, Flex, IconButton } from "theme-ui"; import Konva from "konva"; -import Slider from "../Slider"; import TextareaAutosize from "../TextareaAutoSize"; import MapMenu from "../map/MapMenu"; @@ -27,8 +26,6 @@ import { import { Note } from "../../types/Note"; import { Map } from "../../types/Map"; -const defaultNoteMaxSize = 6; - type NoteMenuProps = { isOpen: boolean; onRequestClose: RequestCloseEventHandler; @@ -50,12 +47,10 @@ function NoteMenu({ 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(); @@ -64,8 +59,8 @@ function NoteMenu({ 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); + // Y 20px from the bottom + setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 20); } } } @@ -83,11 +78,6 @@ function NoteMenu({ onNoteChange({ [note.id]: { color: color } }); } - function handleSizeChange(event: React.ChangeEvent) { - const newSize = parseFloat(event.target.value); - note && onNoteChange({ [note.id]: { size: newSize } }); - } - function handleVisibleChange() { note && onNoteChange({ [note.id]: { visible: !note.visible } }); } @@ -198,24 +188,6 @@ function NoteMenu({ ))} - - - Size: - - - {/* Only show hide and lock token actions to map owners */} {map && map.owner === userId && ( diff --git a/src/components/tools/NoteTool.tsx b/src/components/tools/NoteTool.tsx index 71bc922..2669f09 100644 --- a/src/components/tools/NoteTool.tsx +++ b/src/components/tools/NoteTool.tsx @@ -18,8 +18,6 @@ import { Map } from "../../types/Map"; import { Note as NoteType } from "../../types/Note"; import { NoteCreateEventHander, - NoteChangeEventHandler, - NoteDragEventHandler, NoteMenuOpenEventHandler, } from "../../types/Events"; @@ -29,26 +27,16 @@ type MapNoteProps = { map: Map | null; active: boolean; onNoteCreate: NoteCreateEventHander; - onNoteChange: NoteChangeEventHandler; - notes: NoteType[]; onNoteMenuOpen: NoteMenuOpenEventHandler; - draggable: boolean; - onNoteDragStart: NoteDragEventHandler; - onNoteDragEnd: NoteDragEventHandler; - fadeOnHover: boolean; + children: React.ReactNode; }; function NoteTool({ map, active, onNoteCreate, - onNoteChange, - notes, onNoteMenuOpen, - draggable, - onNoteDragStart, - onNoteDragEnd, - fadeOnHover, + children, }: MapNoteProps) { const interactionEmitter = useInteractionEmitter(); const userId = useUserId(); @@ -101,6 +89,7 @@ function NoteTool({ locked: false, color: "yellow", textOnly: false, + rotation: 0, }); setIsBrushDown(true); } @@ -147,21 +136,11 @@ function NoteTool({ return ( - {notes.map((note) => ( - - ))} + {children} - {isBrushDown && noteData && } + {isBrushDown && noteData && ( + + )} ); diff --git a/src/hooks/useMapNotes.tsx b/src/hooks/useMapNotes.tsx index c3cfb54..52bf2a0 100644 --- a/src/hooks/useMapNotes.tsx +++ b/src/hooks/useMapNotes.tsx @@ -1,6 +1,7 @@ import Konva from "konva"; import { KonvaEventObject } from "konva/lib/Node"; import { useState } from "react"; +import Note from "../components/konva/Note"; import NoteDragOverlay from "../components/note/NoteDragOverlay"; import NoteMenu from "../components/note/NoteMenu"; import NoteTool from "../components/tools/NoteTool"; @@ -12,7 +13,11 @@ import { } from "../types/Events"; import { Map, MapToolId } from "../types/Map"; import { MapState } from "../types/MapState"; -import { Note, NoteDraggingOptions, NoteMenuOptions } from "../types/Note"; +import { + Note as NoteType, + NoteDraggingOptions, + NoteMenuOptions, +} from "../types/Note"; function useMapNotes( map: Map | null, @@ -37,6 +42,10 @@ function useMapNotes( setIsNoteMenuOpen(true); } + function handleNoteMenuClose() { + setIsNoteMenuOpen(false); + } + function handleNoteDragStart(_: KonvaEventObject, noteId: string) { setNoteDraggingOptions({ dragging: true, noteId }); } @@ -56,29 +65,43 @@ function useMapNotes( map={map} active={selectedToolId === "note"} onNoteCreate={onNoteCreate} - onNoteChange={onNoteChange} - notes={ - mapState - ? Object.values(mapState.notes).sort((a, b) => - sortNotes(a, b, noteDraggingOptions) - ) - : [] - } onNoteMenuOpen={handleNoteMenuOpen} - draggable={ - allowNoteEditing && - (selectedToolId === "note" || selectedToolId === "move") - } - onNoteDragStart={handleNoteDragStart} - onNoteDragEnd={handleNoteDragEnd} - fadeOnHover={selectedToolId === "drawing"} - /> + > + {(mapState + ? Object.values(mapState.notes).sort((a, b) => + sortNotes(a, b, noteDraggingOptions) + ) + : [] + ).map((note) => ( + + ))} + ); const noteMenu = ( setIsNoteMenuOpen(false)} + onRequestClose={handleNoteMenuClose} onNoteChange={onNoteChange} note={noteMenuOptions && mapState?.notes[noteMenuOptions.noteId]} noteNode={noteMenuOptions?.noteNode} @@ -100,8 +123,8 @@ function useMapNotes( export default useMapNotes; function sortNotes( - a: Note, - b: Note, + a: NoteType, + b: NoteType, noteDraggingOptions?: NoteDraggingOptions ) { if ( diff --git a/src/types/Events.ts b/src/types/Events.ts index f5c8f06..f3a1d7c 100644 --- a/src/types/Events.ts +++ b/src/types/Events.ts @@ -46,6 +46,7 @@ export type NoteMenuOpenEventHandler = ( noteId: string, noteNode: Konva.Node ) => void; +export type NoteMenuCloseEventHandler = () => void; export type NoteDragEventHandler = ( event: Konva.KonvaEventObject, noteId: string diff --git a/src/types/Note.ts b/src/types/Note.ts index 424774b..7d54409 100644 --- a/src/types/Note.ts +++ b/src/types/Note.ts @@ -13,6 +13,7 @@ export type Note = { visible: boolean; x: number; y: number; + rotation: number; }; export type NoteMenuOptions = { diff --git a/src/upgrade.ts b/src/upgrade.ts index 61d70a0..c1b7c44 100644 --- a/src/upgrade.ts +++ b/src/upgrade.ts @@ -834,9 +834,22 @@ export const versions: Record = { _uncommittedChanges: null, }); }, + // v1.10.0 - Add rotation to notes + 37(v, onUpgrade) { + v.stores({}).upgrade((tx) => { + onUpgrade?.(37); + tx.table("states") + .toCollection() + .modify((state) => { + for (let id in state.notes) { + state.notes[id].rotation = 0; + } + }); + }); + }, }; -export const latestVersion = 36; +export const latestVersion = 37; /** * Load versions onto a database up to a specific version number diff --git a/src/validators/Note.ts b/src/validators/Note.ts index a55601c..f9b966e 100644 --- a/src/validators/Note.ts +++ b/src/validators/Note.ts @@ -40,6 +40,9 @@ export const NoteSchema: JSONSchemaType = { y: { type: "number", }, + rotation: { + type: "number", + }, }, required: [ "color", @@ -53,6 +56,7 @@ export const NoteSchema: JSONSchemaType = { "visible", "x", "y", + "rotation", ], type: "object", };