From d076d5417090fc3a2a1bfc97dbea8ea6ef21075e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 4 Oct 2025 09:15:15 +0300 Subject: [PATCH] refactor(react/type_widgets): extract context menu to separate file --- .../type_widgets/relation_map/NoteBox.tsx | 51 ++--------------- .../type_widgets/relation_map/RelationMap.tsx | 27 +-------- .../type_widgets/relation_map/context_menu.ts | 56 +++++++++++++++++++ .../type_widgets/relation_map/utils.ts | 27 +++++++++ 4 files changed, 88 insertions(+), 73 deletions(-) create mode 100644 apps/client/src/widgets/type_widgets/relation_map/context_menu.ts diff --git a/apps/client/src/widgets/type_widgets/relation_map/NoteBox.tsx b/apps/client/src/widgets/type_widgets/relation_map/NoteBox.tsx index ad069afc0..fa65926d0 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/NoteBox.tsx +++ b/apps/client/src/widgets/type_widgets/relation_map/NoteBox.tsx @@ -1,17 +1,14 @@ -import { useCallback, useEffect, useState } from "preact/hooks"; +import { useEffect, useMemo, useState } from "preact/hooks"; import { useNoteProperty } from "../../react/hooks"; import froca from "../../../services/froca"; -import contextMenu from "../../../menus/context_menu"; import { t } from "../../../services/i18n"; -import appContext from "../../../components/app_context"; -import dialog from "../../../services/dialog"; -import server from "../../../services/server"; import { JsPlumbItem } from "./jsplumb"; import FNote from "../../../entities/fnote"; import RelationMapApi, { MapDataNoteEntry } from "./api"; import { RefObject } from "preact"; import NoteLink from "../../react/NoteLink"; import { idToNoteId, noteIdToId } from "./utils"; +import { buildNoteContextMenuHandler } from "./context_menu"; const NOTE_BOX_SOURCE_CONFIG = { filter: ".endpoint", @@ -40,48 +37,8 @@ export function NoteBox({ noteId, x, y, mapApiRef }: NoteBoxProps) { froca.getNote(noteId).then(setNote); }, [ noteId ]); - const contextMenuHandler = useCallback((e: MouseEvent) => { - e.preventDefault(); - contextMenu.show({ - x: e.pageX, - y: e.pageY, - items: [ - { - title: t("relation_map.open_in_new_tab"), - uiIcon: "bx bx-empty", - handler: () => appContext.tabManager.openTabWithNoteWithHoisting(noteId) - }, - { - title: t("relation_map.remove_note"), - uiIcon: "bx bx-trash", - handler: async () => { - if (!note) return; - const result = await dialog.confirmDeleteNoteBoxWithNote(note.title); - if (typeof result !== "object" || !result.confirmed) return; - - mapApiRef.current?.removeItem(noteId, result.isDeleteNoteChecked); - } - }, - { - title: t("relation_map.edit_title"), - uiIcon: "bx bx-pencil", - handler: async () => { - const title = await dialog.prompt({ - title: t("relation_map.rename_note"), - message: t("relation_map.enter_new_title"), - defaultValue: note?.title, - }); - - if (!title) { - return; - } - - await server.put(`notes/${noteId}/title`, { title }); - } - } - ], - selectMenuItemHandler() {} - }) + const contextMenuHandler = useMemo(() => { + return buildNoteContextMenuHandler(note, mapApiRef); }, [ note ]); return note && ( diff --git a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx index 3739e353d..27d812560 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx +++ b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx @@ -14,7 +14,7 @@ import { CreateChildrenResponse, RelationMapPostResponse, RelationMapRelation } import RelationMapApi, { MapData, MapDataNoteEntry } from "./api"; import setupOverlays, { uniDirectionalOverlays } from "./overlays"; import { JsPlumb } from "./jsplumb"; -import { noteIdToId } from "./utils"; +import { getMousePosition, getZoom, noteIdToId } from "./utils"; import { NoteBox } from "./NoteBox"; interface Clipboard { @@ -304,28 +304,3 @@ function useNoteCreation({ ntxId, note, containerRef, mapApiRef }: { }, []); return onClickHandler; } - -function getZoom(container: HTMLDivElement) { - const transform = window.getComputedStyle(container).transform; - if (transform === "none") { - return 1; - } - - const matrixRegex = /matrix\((-?\d*\.?\d+),\s*0,\s*0,\s*-?\d*\.?\d+,\s*-?\d*\.?\d+,\s*-?\d*\.?\d+\)/; - const matches = transform.match(matrixRegex); - - if (!matches) { - throw new Error(t("relation_map.cannot_match_transform", { transform })); - } - - return parseFloat(matches[1]); -} - -function getMousePosition(evt: MouseEvent, container: HTMLDivElement, zoom: number) { - const rect = container.getBoundingClientRect(); - - return { - x: ((evt.clientX ?? 0) - rect.left) / zoom, - y: ((evt.clientY ?? 0) - rect.top) / zoom - }; -} diff --git a/apps/client/src/widgets/type_widgets/relation_map/context_menu.ts b/apps/client/src/widgets/type_widgets/relation_map/context_menu.ts new file mode 100644 index 000000000..3c27cb13c --- /dev/null +++ b/apps/client/src/widgets/type_widgets/relation_map/context_menu.ts @@ -0,0 +1,56 @@ +import { RefObject } from "preact"; +import appContext from "../../../components/app_context"; +import FNote from "../../../entities/fnote"; +import contextMenu from "../../../menus/context_menu"; +import dialog from "../../../services/dialog"; +import { t } from "../../../services/i18n"; +import server from "../../../services/server"; +import RelationMapApi from "./api"; + +export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject) { + return (e: MouseEvent) => { + if (!note) return; + e.preventDefault(); + + contextMenu.show({ + x: e.pageX, + y: e.pageY, + items: [ + { + title: t("relation_map.open_in_new_tab"), + uiIcon: "bx bx-empty", + handler: () => appContext.tabManager.openTabWithNoteWithHoisting(note.noteId) + }, + { + title: t("relation_map.remove_note"), + uiIcon: "bx bx-trash", + handler: async () => { + if (!note) return; + const result = await dialog.confirmDeleteNoteBoxWithNote(note.title); + if (typeof result !== "object" || !result.confirmed) return; + + mapApiRef.current?.removeItem(note.noteId, result.isDeleteNoteChecked); + } + }, + { + title: t("relation_map.edit_title"), + uiIcon: "bx bx-pencil", + handler: async () => { + const title = await dialog.prompt({ + title: t("relation_map.rename_note"), + message: t("relation_map.enter_new_title"), + defaultValue: note?.title, + }); + + if (!title) { + return; + } + + await server.put(`notes/${note.noteId}/title`, { title }); + } + } + ], + selectMenuItemHandler() {} + }) + }; +} diff --git a/apps/client/src/widgets/type_widgets/relation_map/utils.ts b/apps/client/src/widgets/type_widgets/relation_map/utils.ts index 0564628ad..e52a40fbf 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/utils.ts +++ b/apps/client/src/widgets/type_widgets/relation_map/utils.ts @@ -1,3 +1,5 @@ +import { t } from "../../../services/i18n"; + export function noteIdToId(noteId: string) { return `rel-map-note-${noteId}`; } @@ -5,3 +7,28 @@ export function noteIdToId(noteId: string) { export function idToNoteId(id: string) { return id.substr(13); } + +export function getZoom(container: HTMLDivElement) { + const transform = window.getComputedStyle(container).transform; + if (transform === "none") { + return 1; + } + + const matrixRegex = /matrix\((-?\d*\.?\d+),\s*0,\s*0,\s*-?\d*\.?\d+,\s*-?\d*\.?\d+,\s*-?\d*\.?\d+\)/; + const matches = transform.match(matrixRegex); + + if (!matches) { + throw new Error(t("relation_map.cannot_match_transform", { transform })); + } + + return parseFloat(matches[1]); +} + +export function getMousePosition(evt: MouseEvent, container: HTMLDivElement, zoom: number) { + const rect = container.getBoundingClientRect(); + + return { + x: ((evt.clientX ?? 0) - rect.left) / zoom, + y: ((evt.clientY ?? 0) - rect.top) / zoom + }; +}