chore(react/collections/table): set up context menu partially

This commit is contained in:
Elian Doran 2025-09-06 20:25:50 +03:00
parent 9d877ec97a
commit 76e903a782
No known key found for this signature in database
3 changed files with 72 additions and 55 deletions

View File

@ -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 contextMenu, { MenuItem } from "../../../menus/context_menu.js";
import { TableData } from "./rows.js"; import FNote from "../../../entities/fnote.js";
import branches from "../../../services/branches.js";
import { t } from "../../../services/i18n.js"; import { t } from "../../../services/i18n.js";
import { TableData } from "./rows.js";
import link_context_menu from "../../../menus/link_context_menu.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 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) { export function useContextMenu(parentNote: FNote, parentComponent: Component | null | undefined, tabulator: RefObject<Tabulator>): Partial<EventCallBackMethods> {
tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator)); const events: Partial<EventCallBackMethods> = {};
tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, parentNote, tabulator)); if (!tabulator || !parentComponent) return events;
tabulator.on("renderComplete", () => {
const headerRow = tabulator.element.querySelector(".tabulator-header-contents");
headerRow?.addEventListener("contextmenu", (e) => showHeaderContextMenu(e, tabulator));
});
// Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't. events["rowContext"] = (e, row) => tabulator.current && showRowContextMenu(parentComponent, e as MouseEvent, row, parentNote, tabulator.current);
if (tabulator.options.dataTree) { events["headerContext"] = (e, col) => tabulator.current && showColumnContextMenu(parentComponent, e as MouseEvent, col, parentNote, tabulator.current);
const dismissContextMenu = () => contextMenu.hide(); events["renderComplete"] = () => {
tabulator.on("dataTreeRowExpanded", dismissContextMenu); const headerRow = tabulator.current?.element.querySelector(".tabulator-header-contents");
tabulator.on("dataTreeRowCollapsed", dismissContextMenu); 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) { function showColumnContextMenu(parentComponent: Component, e: MouseEvent, column: ColumnComponent, parentNote: FNote, tabulator: Tabulator) {
const e = _e as MouseEvent;
const { title, field } = column.getDefinition(); const { title, field } = column.getDefinition();
const sorters = tabulator.getSorters(); const sorters = tabulator.getSorters();
@ -87,16 +91,16 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote:
title: t("table_view.add-column-to-the-left"), title: t("table_view.add-column-to-the-left"),
uiIcon: "bx bx-horizontal-left", uiIcon: "bx bx-horizontal-left",
enabled: !column.getDefinition().frozen, enabled: !column.getDefinition().frozen,
items: buildInsertSubmenu(e, column, "before"), items: buildInsertSubmenu(parentComponent, column, "before"),
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { handler: () => parentComponent?.triggerCommand("addNewTableColumn", {
referenceColumn: column referenceColumn: column
}) })
}, },
{ {
title: t("table_view.add-column-to-the-right"), title: t("table_view.add-column-to-the-right"),
uiIcon: "bx bx-horizontal-right", uiIcon: "bx bx-horizontal-right",
items: buildInsertSubmenu(e, column, "after"), items: buildInsertSubmenu(parentComponent, column, "after"),
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { handler: () => parentComponent?.triggerCommand("addNewTableColumn", {
referenceColumn: column, referenceColumn: column,
direction: "after" direction: "after"
}) })
@ -106,7 +110,7 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote:
title: t("table_view.edit-column"), title: t("table_view.edit-column"),
uiIcon: "bx bxs-edit-alt", uiIcon: "bx bxs-edit-alt",
enabled: isUserDefinedColumn, enabled: isUserDefinedColumn,
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { handler: () => parentComponent?.triggerCommand("addNewTableColumn", {
referenceColumn: column, referenceColumn: column,
columnToEdit: column columnToEdit: column
}) })
@ -115,7 +119,7 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote:
title: t("table_view.delete-column"), title: t("table_view.delete-column"),
uiIcon: "bx bx-trash", uiIcon: "bx bx-trash",
enabled: isUserDefinedColumn, enabled: isUserDefinedColumn,
handler: () => getParentComponent(e)?.triggerCommand("deleteTableColumn", { handler: () => parentComponent?.triggerCommand("deleteTableColumn", {
columnToDelete: column 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). * 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. * Provides generic options such as toggling columns.
*/ */
function showHeaderContextMenu(_e: Event, tabulator: Tabulator) { function showHeaderContextMenu(parentComponent: Component, e: MouseEvent, tabulator: Tabulator) {
const e = _e as MouseEvent;
contextMenu.show({ contextMenu.show({
items: [ items: [
{ {
@ -146,7 +149,7 @@ function showHeaderContextMenu(_e: Event, tabulator: Tabulator) {
uiIcon: "bx bx-empty", uiIcon: "bx bx-empty",
enabled: false enabled: false
}, },
...buildInsertSubmenu(e) ...buildInsertSubmenu(parentComponent)
], ],
selectMenuItemHandler() {}, selectMenuItemHandler() {},
x: e.pageX, x: e.pageX,
@ -155,8 +158,7 @@ function showHeaderContextMenu(_e: Event, tabulator: Tabulator) {
e.preventDefault(); e.preventDefault();
} }
export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) { export function showRowContextMenu(parentComponent: Component, e: MouseEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) {
const e = _e as MouseEvent;
const rowData = row.getData() as TableData; const rowData = row.getData() as TableData;
let parentNoteId: string = parentNote.noteId; 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"), title: t("table_view.row-insert-above"),
uiIcon: "bx bx-horizontal-left bx-rotate-90", uiIcon: "bx bx-horizontal-left bx-rotate-90",
handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { handler: () => parentComponent?.triggerCommand("addNewRow", {
parentNotePath: parentNoteId, parentNotePath: parentNoteId,
customOpts: { customOpts: {
target: "before", target: "before",
@ -189,7 +191,7 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F
handler: async () => { handler: async () => {
const branchId = row.getData().branchId; const branchId = row.getData().branchId;
const note = await froca.getBranch(branchId)?.getNote(); const note = await froca.getBranch(branchId)?.getNote();
getParentComponent(e)?.triggerCommand("addNewRow", { parentComponent?.triggerCommand("addNewRow", {
parentNotePath: note?.noteId, parentNotePath: note?.noteId,
customOpts: { customOpts: {
target: "after", target: "after",
@ -201,7 +203,7 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F
{ {
title: t("table_view.row-insert-below"), title: t("table_view.row-insert-below"),
uiIcon: "bx bx-horizontal-left bx-rotate-270", uiIcon: "bx bx-horizontal-left bx-rotate-270",
handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { handler: () => parentComponent?.triggerCommand("addNewRow", {
parentNotePath: parentNoteId, parentNotePath: parentNoteId,
customOpts: { customOpts: {
target: "after", target: "after",
@ -223,16 +225,6 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F
e.preventDefault(); e.preventDefault();
} }
function getParentComponent(e: MouseEvent) {
if (!e.target) {
return;
}
return $(e.target)
.closest(".component")
.prop("component") as Component;
}
function buildColumnItems(tabulator: Tabulator) { function buildColumnItems(tabulator: Tabulator) {
const items: MenuItem<unknown>[] = []; const items: MenuItem<unknown>[] = [];
for (const column of tabulator.getColumns()) { for (const column of tabulator.getColumns()) {
@ -249,13 +241,13 @@ function buildColumnItems(tabulator: Tabulator) {
return items; return items;
} }
function buildInsertSubmenu(e: MouseEvent, referenceColumn?: ColumnComponent, direction?: "before" | "after"): MenuItem<unknown>[] { function buildInsertSubmenu(parentComponent: Component, referenceColumn?: ColumnComponent, direction?: "before" | "after"): MenuItem<unknown>[] {
return [ return [
{ {
title: t("table_view.new-column-label"), title: t("table_view.new-column-label"),
uiIcon: "bx bx-hash", uiIcon: "bx bx-hash",
handler: () => { handler: () => {
getParentComponent(e)?.triggerCommand("addNewTableColumn", { parentComponent?.triggerCommand("addNewTableColumn", {
referenceColumn, referenceColumn,
type: "label", type: "label",
direction direction
@ -266,7 +258,7 @@ function buildInsertSubmenu(e: MouseEvent, referenceColumn?: ColumnComponent, di
title: t("table_view.new-column-relation"), title: t("table_view.new-column-relation"),
uiIcon: "bx bx-transfer", uiIcon: "bx bx-transfer",
handler: () => { handler: () => {
getParentComponent(e)?.triggerCommand("addNewTableColumn", { parentComponent?.triggerCommand("addNewTableColumn", {
referenceColumn, referenceColumn,
type: "relation", type: "relation",
direction direction

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useRef, useState } from "preact/hooks";
import { ViewModeProps } from "../interface"; import { ViewModeProps } from "../interface";
import "./index.css"; import "./index.css";
import { buildColumnDefinitions } from "./columns"; import { buildColumnDefinitions } from "./columns";
@ -6,8 +6,9 @@ import getAttributeDefinitionInformation, { buildRowDefinitions, TableData } fro
import { useNoteLabelInt } from "../../react/hooks"; import { useNoteLabelInt } from "../../react/hooks";
import { canReorderRows } from "../../view_widgets/table_view/dragging"; import { canReorderRows } from "../../view_widgets/table_view/dragging";
import Tabulator from "./tabulator"; 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 { interface TableConfig {
tableData?: { tableData?: {
columns?: ColumnDefinition[]; columns?: ColumnDefinition[];
@ -18,6 +19,8 @@ export default function TableView({ note, viewConfig }: ViewModeProps<TableConfi
const [ maxDepth ] = useNoteLabelInt(note, "maxNestingDepth") ?? -1; const [ maxDepth ] = useNoteLabelInt(note, "maxNestingDepth") ?? -1;
const [ columnDefs, setColumnDefs ] = useState<ColumnDefinition[]>(); const [ columnDefs, setColumnDefs ] = useState<ColumnDefinition[]>();
const [ rowData, setRowData ] = useState<TableData[]>(); const [ rowData, setRowData ] = useState<TableData[]>();
const tabulatorRef = useRef<VanillaTabulator>(null);
const parentComponent = useContext(ParentComponent);
useEffect(() => { useEffect(() => {
const info = getAttributeDefinitionInformation(note); const info = getAttributeDefinitionInformation(note);
@ -34,14 +37,18 @@ export default function TableView({ note, viewConfig }: ViewModeProps<TableConfi
}); });
}, [ note ]); }, [ note ]);
const contextMenuEvents = useContextMenu(note, parentComponent, tabulatorRef);
return ( return (
<div className="table-view"> <div className="table-view">
{columnDefs && ( {columnDefs && (
<Tabulator <Tabulator
tabulatorRef={tabulatorRef}
className="table-view-container" className="table-view-container"
columns={columnDefs} columns={columnDefs}
data={rowData} data={rowData}
modules={[ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ]} modules={[ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ]}
{...contextMenuEvents}
/> />
)} )}
</div> </div>

View File

@ -1,27 +1,29 @@
import { useEffect, useRef } from "preact/hooks"; import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
import { ColumnDefinition, Module, Tabulator as VanillaTabulator } from "tabulator-tables"; import { ColumnDefinition, EventCallBackMethods, Module, Tabulator as VanillaTabulator } from "tabulator-tables";
import "tabulator-tables/dist/css/tabulator.css"; import "tabulator-tables/dist/css/tabulator.css";
import "../../../../src/stylesheets/table.css"; import "../../../../src/stylesheets/table.css";
import { RefObject } from "preact";
interface TableProps<T> { interface TableProps<T> extends Partial<EventCallBackMethods> {
tabulatorRef: RefObject<VanillaTabulator>;
className?: string; className?: string;
columns: ColumnDefinition[]; columns: ColumnDefinition[];
data?: T[]; data?: T[];
modules?: (new (table: VanillaTabulator) => Module)[]; modules?: (new (table: VanillaTabulator) => Module)[];
} }
export default function Tabulator<T>({ className, columns, data, modules }: TableProps<T>) { export default function Tabulator<T>({ className, columns, data, modules, tabulatorRef: externalTabulatorRef, ...events }: TableProps<T>) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const tabulatorRef = useRef<VanillaTabulator>(null); const tabulatorRef = useRef<VanillaTabulator>(null);
useEffect(() => { useLayoutEffect(() => {
if (!modules) return; if (!modules) return;
for (const module of modules) { for (const module of modules) {
VanillaTabulator.registerModule(module); VanillaTabulator.registerModule(module);
} }
}, [modules]); }, [modules]);
useEffect(() => { useLayoutEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
const tabulator = new VanillaTabulator(containerRef.current, { const tabulator = new VanillaTabulator(containerRef.current, {
@ -30,10 +32,26 @@ export default function Tabulator<T>({ className, columns, data, modules }: Tabl
}); });
tabulatorRef.current = tabulator; tabulatorRef.current = tabulator;
externalTabulatorRef.current = tabulator;
return () => tabulator.destroy(); 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 ( return (
<div ref={containerRef} className={className} /> <div ref={containerRef} className={className} />
); );