import { ColumnComponent, EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables"; import contextMenu, { MenuItem } from "../../../menus/context_menu.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 froca from "../../../services/froca.js"; import branches from "../../../services/branches.js"; import Component from "../../../components/component.js"; import { RefObject } from "preact"; export function useContextMenu(parentNote: FNote, parentComponent: Component | null | undefined, tabulator: RefObject): Partial { const events: Partial = {}; if (!tabulator || !parentComponent) return events; 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(parentComponent: Component, e: MouseEvent, column: ColumnComponent, parentNote: FNote, tabulator: Tabulator) { const { title, field } = column.getDefinition(); const sorters = tabulator.getSorters(); const sorter = sorters.find(sorter => sorter.field === field); const isUserDefinedColumn = (!!field && (field?.startsWith("labels.") || field?.startsWith("relations."))); contextMenu.show({ items: [ { title: t("table_view.sort-column-by", { title }), enabled: !!field, uiIcon: "bx bx-sort-alt-2", items: [ { title: t("table_view.sort-column-ascending"), checked: (sorter?.dir === "asc"), uiIcon: "bx bx-empty", handler: () => tabulator.setSort([ { column: field!, dir: "asc", } ]) }, { title: t("table_view.sort-column-descending"), checked: (sorter?.dir === "desc"), uiIcon: "bx bx-empty", handler: () => tabulator.setSort([ { column: field!, dir: "desc" } ]) } ] }, { title: t("table_view.sort-column-clear"), enabled: sorters.length > 0, uiIcon: "bx bx-x-circle", handler: () => tabulator.clearSort() }, { title: "----" }, { title: t("table_view.hide-column", { title }), uiIcon: "bx bx-hide", handler: () => column.hide() }, { title: t("table_view.show-hide-columns"), uiIcon: "bx bx-columns", items: buildColumnItems(tabulator) }, { title: "----" }, { title: t("table_view.add-column-to-the-left"), uiIcon: "bx bx-horizontal-left", enabled: !column.getDefinition().frozen, 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(parentComponent, column, "after"), handler: () => parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn: column, direction: "after" }) }, { title: "----" }, { title: t("table_view.edit-column"), uiIcon: "bx bxs-edit-alt", enabled: isUserDefinedColumn, handler: () => parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn: column, columnToEdit: column }) }, { title: t("table_view.delete-column"), uiIcon: "bx bx-trash", enabled: isUserDefinedColumn, handler: () => parentComponent?.triggerCommand("deleteTableColumn", { columnToDelete: column }) } ], selectMenuItemHandler() {}, x: e.pageX, y: e.pageY }); e.preventDefault(); } /** * 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(parentComponent: Component, e: MouseEvent, tabulator: Tabulator) { contextMenu.show({ items: [ { title: t("table_view.show-hide-columns"), uiIcon: "bx bx-columns", items: buildColumnItems(tabulator) }, { title: "----" }, { title: t("table_view.new-column"), uiIcon: "bx bx-empty", enabled: false }, ...buildInsertSubmenu(parentComponent) ], selectMenuItemHandler() {}, x: e.pageX, y: e.pageY }); e.preventDefault(); } export function showRowContextMenu(parentComponent: Component, e: MouseEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) { const rowData = row.getData() as TableData; let parentNoteId: string = parentNote.noteId; if (tabulator.options.dataTree) { const parentRow = row.getTreeParent(); if (parentRow) { parentNoteId = parentRow.getData().noteId as string; } } contextMenu.show({ items: [ ...link_context_menu.getItems(), { title: "----" }, { title: t("table_view.row-insert-above"), uiIcon: "bx bx-horizontal-left bx-rotate-90", handler: () => parentComponent?.triggerCommand("addNewRow", { parentNotePath: parentNoteId, customOpts: { target: "before", targetBranchId: rowData.branchId, } }) }, { title: t("table_view.row-insert-child"), uiIcon: "bx bx-subdirectory-right", handler: async () => { const branchId = row.getData().branchId; const note = await froca.getBranch(branchId)?.getNote(); parentComponent?.triggerCommand("addNewRow", { parentNotePath: note?.noteId, customOpts: { target: "after", targetBranchId: branchId, } }); } }, { title: t("table_view.row-insert-below"), uiIcon: "bx bx-horizontal-left bx-rotate-270", handler: () => parentComponent?.triggerCommand("addNewRow", { parentNotePath: parentNoteId, customOpts: { target: "after", targetBranchId: rowData.branchId, } }) }, { title: "----" }, { title: t("table_context_menu.delete_row"), uiIcon: "bx bx-trash", handler: () => branches.deleteNotes([ rowData.branchId ], false, false) } ], selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, rowData.noteId), x: e.pageX, y: e.pageY }); e.preventDefault(); } function buildColumnItems(tabulator: Tabulator) { const items: MenuItem[] = []; for (const column of tabulator.getColumns()) { const { title } = column.getDefinition(); items.push({ title, checked: column.isVisible(), uiIcon: "bx bx-empty", handler: () => column.toggle() }); } return items; } function buildInsertSubmenu(parentComponent: Component, referenceColumn?: ColumnComponent, direction?: "before" | "after"): MenuItem[] { return [ { title: t("table_view.new-column-label"), uiIcon: "bx bx-hash", handler: () => { parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn, type: "label", direction }); } }, { title: t("table_view.new-column-relation"), uiIcon: "bx bx-transfer", handler: () => { parentComponent?.triggerCommand("addNewTableColumn", { referenceColumn, type: "relation", direction }); } } ] }