From 76e903a78266cfca9f857da42679c3e4c5288cd2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Sep 2025 20:25:50 +0300 Subject: [PATCH] chore(react/collections/table): set up context menu partially --- .../table}/context_menu.ts | 84 +++++++++---------- .../src/widgets/collections/table/index.tsx | 13 ++- .../widgets/collections/table/tabulator.tsx | 30 +++++-- 3 files changed, 72 insertions(+), 55 deletions(-) rename apps/client/src/widgets/{view_widgets/table_view => collections/table}/context_menu.ts (74%) diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/collections/table/context_menu.ts similarity index 74% rename from apps/client/src/widgets/view_widgets/table_view/context_menu.ts rename to apps/client/src/widgets/collections/table/context_menu.ts index 21f434d7d..37caf1663 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/collections/table/context_menu.ts @@ -1,31 +1,35 @@ -import { ColumnComponent, RowComponent, Tabulator } from "tabulator-tables"; +import { ColumnComponent, EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables"; import contextMenu, { MenuItem } from "../../../menus/context_menu.js"; -import { TableData } from "./rows.js"; -import branches from "../../../services/branches.js"; +import FNote from "../../../entities/fnote.js"; import { t } from "../../../services/i18n.js"; +import { TableData } from "./rows.js"; import link_context_menu from "../../../menus/link_context_menu.js"; -import type FNote from "../../../entities/fnote.js"; import froca from "../../../services/froca.js"; -import type Component from "../../../components/component.js"; +import branches from "../../../services/branches.js"; +import Component from "../../../components/component.js"; +import { RefObject } from "preact"; -export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { - tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator)); - tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, parentNote, tabulator)); - tabulator.on("renderComplete", () => { - const headerRow = tabulator.element.querySelector(".tabulator-header-contents"); - headerRow?.addEventListener("contextmenu", (e) => showHeaderContextMenu(e, tabulator)); - }); +export function useContextMenu(parentNote: FNote, parentComponent: Component | null | undefined, tabulator: RefObject): Partial { + const events: Partial = {}; + if (!tabulator || !parentComponent) return events; - // Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't. - if (tabulator.options.dataTree) { - const dismissContextMenu = () => contextMenu.hide(); - tabulator.on("dataTreeRowExpanded", dismissContextMenu); - tabulator.on("dataTreeRowCollapsed", dismissContextMenu); + events["rowContext"] = (e, row) => tabulator.current && showRowContextMenu(parentComponent, e as MouseEvent, row, parentNote, tabulator.current); + events["headerContext"] = (e, col) => tabulator.current && showColumnContextMenu(parentComponent, e as MouseEvent, col, parentNote, tabulator.current); + events["renderComplete"] = () => { + const headerRow = tabulator.current?.element.querySelector(".tabulator-header-contents"); + headerRow?.addEventListener("contextmenu", (e) => showHeaderContextMenu(parentComponent, e as MouseEvent, tabulator.current!)); } + // Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't. + if (tabulator.current?.options.dataTree) { + const dismissContextMenu = () => contextMenu.hide(); + events["dataTreeRowExpanded"] = dismissContextMenu; + events["dataTreeRowCollapsed"] = dismissContextMenu; + } + + return events; } -function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: FNote, tabulator: Tabulator) { - const e = _e as MouseEvent; +function showColumnContextMenu(parentComponent: Component, e: MouseEvent, column: ColumnComponent, parentNote: FNote, tabulator: Tabulator) { const { title, field } = column.getDefinition(); const sorters = tabulator.getSorters(); @@ -87,16 +91,16 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: title: t("table_view.add-column-to-the-left"), uiIcon: "bx bx-horizontal-left", enabled: !column.getDefinition().frozen, - items: buildInsertSubmenu(e, column, "before"), - handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { + items: buildInsertSubmenu(parentComponent, column, "before"), + handler: () => parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn: column }) }, { title: t("table_view.add-column-to-the-right"), uiIcon: "bx bx-horizontal-right", - items: buildInsertSubmenu(e, column, "after"), - handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { + items: buildInsertSubmenu(parentComponent, column, "after"), + handler: () => parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn: column, direction: "after" }) @@ -106,7 +110,7 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: title: t("table_view.edit-column"), uiIcon: "bx bxs-edit-alt", enabled: isUserDefinedColumn, - handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { + handler: () => parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn: column, columnToEdit: column }) @@ -115,7 +119,7 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: title: t("table_view.delete-column"), uiIcon: "bx bx-trash", enabled: isUserDefinedColumn, - handler: () => getParentComponent(e)?.triggerCommand("deleteTableColumn", { + handler: () => parentComponent?.triggerCommand("deleteTableColumn", { columnToDelete: column }) } @@ -131,8 +135,7 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: * Shows a context menu which has options dedicated to the header area (the part where the columns are, but in the empty space). * Provides generic options such as toggling columns. */ -function showHeaderContextMenu(_e: Event, tabulator: Tabulator) { - const e = _e as MouseEvent; +function showHeaderContextMenu(parentComponent: Component, e: MouseEvent, tabulator: Tabulator) { contextMenu.show({ items: [ { @@ -146,7 +149,7 @@ function showHeaderContextMenu(_e: Event, tabulator: Tabulator) { uiIcon: "bx bx-empty", enabled: false }, - ...buildInsertSubmenu(e) + ...buildInsertSubmenu(parentComponent) ], selectMenuItemHandler() {}, x: e.pageX, @@ -155,8 +158,7 @@ function showHeaderContextMenu(_e: Event, tabulator: Tabulator) { e.preventDefault(); } -export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) { - const e = _e as MouseEvent; +export function showRowContextMenu(parentComponent: Component, e: MouseEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) { const rowData = row.getData() as TableData; let parentNoteId: string = parentNote.noteId; @@ -175,7 +177,7 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F { title: t("table_view.row-insert-above"), uiIcon: "bx bx-horizontal-left bx-rotate-90", - handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { + handler: () => parentComponent?.triggerCommand("addNewRow", { parentNotePath: parentNoteId, customOpts: { target: "before", @@ -189,7 +191,7 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F handler: async () => { const branchId = row.getData().branchId; const note = await froca.getBranch(branchId)?.getNote(); - getParentComponent(e)?.triggerCommand("addNewRow", { + parentComponent?.triggerCommand("addNewRow", { parentNotePath: note?.noteId, customOpts: { target: "after", @@ -201,7 +203,7 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F { title: t("table_view.row-insert-below"), uiIcon: "bx bx-horizontal-left bx-rotate-270", - handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { + handler: () => parentComponent?.triggerCommand("addNewRow", { parentNotePath: parentNoteId, customOpts: { target: "after", @@ -223,16 +225,6 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F e.preventDefault(); } -function getParentComponent(e: MouseEvent) { - if (!e.target) { - return; - } - - return $(e.target) - .closest(".component") - .prop("component") as Component; -} - function buildColumnItems(tabulator: Tabulator) { const items: MenuItem[] = []; for (const column of tabulator.getColumns()) { @@ -249,13 +241,13 @@ function buildColumnItems(tabulator: Tabulator) { return items; } -function buildInsertSubmenu(e: MouseEvent, referenceColumn?: ColumnComponent, direction?: "before" | "after"): MenuItem[] { +function buildInsertSubmenu(parentComponent: Component, referenceColumn?: ColumnComponent, direction?: "before" | "after"): MenuItem[] { return [ { title: t("table_view.new-column-label"), uiIcon: "bx bx-hash", handler: () => { - getParentComponent(e)?.triggerCommand("addNewTableColumn", { + parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn, type: "label", direction @@ -266,7 +258,7 @@ function buildInsertSubmenu(e: MouseEvent, referenceColumn?: ColumnComponent, di title: t("table_view.new-column-relation"), uiIcon: "bx bx-transfer", handler: () => { - getParentComponent(e)?.triggerCommand("addNewTableColumn", { + parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn, type: "relation", direction diff --git a/apps/client/src/widgets/collections/table/index.tsx b/apps/client/src/widgets/collections/table/index.tsx index dbf5055d0..c887b4fec 100644 --- a/apps/client/src/widgets/collections/table/index.tsx +++ b/apps/client/src/widgets/collections/table/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "preact/hooks"; +import { useContext, useEffect, useRef, useState } from "preact/hooks"; import { ViewModeProps } from "../interface"; import "./index.css"; import { buildColumnDefinitions } from "./columns"; @@ -6,8 +6,9 @@ import getAttributeDefinitionInformation, { buildRowDefinitions, TableData } fro import { useNoteLabelInt } from "../../react/hooks"; import { canReorderRows } from "../../view_widgets/table_view/dragging"; import Tabulator from "./tabulator"; -import {SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule} from 'tabulator-tables'; - +import { Tabulator as VanillaTabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule} from 'tabulator-tables'; +import { useContextMenu } from "./context_menu"; +import { ParentComponent } from "../../react/react_utils"; interface TableConfig { tableData?: { columns?: ColumnDefinition[]; @@ -18,6 +19,8 @@ export default function TableView({ note, viewConfig }: ViewModeProps(); const [ rowData, setRowData ] = useState(); + const tabulatorRef = useRef(null); + const parentComponent = useContext(ParentComponent); useEffect(() => { const info = getAttributeDefinitionInformation(note); @@ -34,14 +37,18 @@ export default function TableView({ note, viewConfig }: ViewModeProps {columnDefs && ( )} diff --git a/apps/client/src/widgets/collections/table/tabulator.tsx b/apps/client/src/widgets/collections/table/tabulator.tsx index a04838c84..8deb1f9e3 100644 --- a/apps/client/src/widgets/collections/table/tabulator.tsx +++ b/apps/client/src/widgets/collections/table/tabulator.tsx @@ -1,27 +1,29 @@ -import { useEffect, useRef } from "preact/hooks"; -import { ColumnDefinition, Module, Tabulator as VanillaTabulator } from "tabulator-tables"; +import { useEffect, useLayoutEffect, useRef } from "preact/hooks"; +import { ColumnDefinition, EventCallBackMethods, Module, Tabulator as VanillaTabulator } from "tabulator-tables"; import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; +import { RefObject } from "preact"; -interface TableProps { +interface TableProps extends Partial { + tabulatorRef: RefObject; className?: string; columns: ColumnDefinition[]; data?: T[]; modules?: (new (table: VanillaTabulator) => Module)[]; } -export default function Tabulator({ className, columns, data, modules }: TableProps) { +export default function Tabulator({ className, columns, data, modules, tabulatorRef: externalTabulatorRef, ...events }: TableProps) { const containerRef = useRef(null); const tabulatorRef = useRef(null); - useEffect(() => { + useLayoutEffect(() => { if (!modules) return; for (const module of modules) { VanillaTabulator.registerModule(module); } }, [modules]); - useEffect(() => { + useLayoutEffect(() => { if (!containerRef.current) return; const tabulator = new VanillaTabulator(containerRef.current, { @@ -30,10 +32,26 @@ export default function Tabulator({ className, columns, data, modules }: Tabl }); tabulatorRef.current = tabulator; + externalTabulatorRef.current = tabulator; return () => tabulator.destroy(); }, []); + useEffect(() => { + const tabulator = tabulatorRef.current; + if (!tabulator) return; + + for (const [ eventName, handler ] of Object.entries(events)) { + tabulator.on(eventName as keyof EventCallBackMethods, handler); + } + + return () => { + for (const [ eventName, handler ] of Object.entries(events)) { + tabulator.off(eventName as keyof EventCallBackMethods, handler); + } + } + }, Object.values(events)); + return (
);