From f4c2d519fcccfb2e80055d2126ab04d7126ebadf Mon Sep 17 00:00:00 2001 From: Vipin Chaudhary Date: Thu, 13 Nov 2025 14:46:27 +0530 Subject: [PATCH] [WIKI-718] feat: add full width options in table block menu (#8087) --- .../components/menus/block-menu-options.tsx | 87 +++++++++++++++++++ .../src/core/components/menus/block-menu.tsx | 18 ++-- 2 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 packages/editor/src/core/components/menus/block-menu-options.tsx diff --git a/packages/editor/src/core/components/menus/block-menu-options.tsx b/packages/editor/src/core/components/menus/block-menu-options.tsx new file mode 100644 index 000000000..d260d8a60 --- /dev/null +++ b/packages/editor/src/core/components/menus/block-menu-options.tsx @@ -0,0 +1,87 @@ +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { TableMap } from "@tiptap/pm/tables"; +import type { Editor } from "@tiptap/react"; +import { MoveHorizontal } from "lucide-react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// types +import type { BlockMenuOption } from "./block-menu"; + +const findSelectedTable = (editor: Editor): { tableNode: ProseMirrorNode | null; tablePos: number } => { + const { state } = editor; + const selectedNode = state.selection.content().content.firstChild; + + if (selectedNode?.type.name === CORE_EXTENSIONS.TABLE) { + return { + tableNode: selectedNode, + tablePos: state.selection.from, + }; + } + + return { tableNode: null, tablePos: -1 }; +}; + +const setTableToFullWidth = (editor: Editor): void => { + try { + const { state, view } = editor; + + // Find the selected table + const { tableNode, tablePos } = findSelectedTable(editor); + if (!tableNode) return; + + // Get content width from CSS variable + const editorContainer = view.dom.closest(".editor-container"); + if (!editorContainer) return; + + const contentWidthVar = getComputedStyle(editorContainer).getPropertyValue("--editor-content-width").trim(); + if (!contentWidthVar) return; + + const contentWidth = parseInt(contentWidthVar); + if (isNaN(contentWidth) || contentWidth <= 0) return; + + // Calculate equal width for each column + const map = TableMap.get(tableNode); + const equalWidth = Math.floor(contentWidth / map.width); + + // Update all cell widths + const tr = state.tr; + const tableStart = tablePos + 1; + const updatedCells = new Set(); + + for (let row = 0; row < map.height; row++) { + for (let col = 0; col < map.width; col++) { + const cellIndex = row * map.width + col; + const cellPos = map.map[cellIndex]; + + // Skip if cell already updated (for merged cells) + if (updatedCells.has(cellPos)) continue; + + const cell = state.doc.nodeAt(tableStart + cellPos); + if (!cell) continue; + + // Handle colspan for merged cells + const colspan = cell.attrs.colspan || 1; + tr.setNodeMarkup(tableStart + cellPos, null, { + ...cell.attrs, + colwidth: Array(colspan).fill(equalWidth), + }); + + updatedCells.add(cellPos); + } + } + + view.dispatch(tr); + } catch (error) { + console.error("Error setting table to full width:", error); + } +}; + +export const getNodeOptions = (editor: Editor): BlockMenuOption[] => [ + { + icon: MoveHorizontal, + key: "table-full-width", + label: "Fit to width", + isDisabled: !editor.isActive(CORE_EXTENSIONS.TABLE), + onClick: () => setTableToFullWidth(editor), + }, +]; diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index 09b60d457..77ee3350b 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -16,12 +16,21 @@ import { cn } from "@plane/utils"; import { CORE_EXTENSIONS } from "@/constants/extension"; // types import type { IEditorProps } from "@/types"; +// components +import { getNodeOptions } from "./block-menu-options"; type Props = { disabledExtensions?: IEditorProps["disabledExtensions"]; editor: Editor; flaggedExtensions?: IEditorProps["flaggedExtensions"]; }; +export type BlockMenuOption = { + icon: LucideIcon; + key: string; + label: string; + onClick: (e: React.MouseEvent) => void; + isDisabled?: boolean; +}; export const BlockMenu = (props: Props) => { const { editor } = props; @@ -130,13 +139,7 @@ export const BlockMenu = (props: Props) => { } }, [isOpen]); - const MENU_ITEMS: { - icon: LucideIcon; - key: string; - label: string; - onClick: (e: React.MouseEvent) => void; - isDisabled?: boolean; - }[] = [ + const MENU_ITEMS: BlockMenuOption[] = [ { icon: Trash2, key: "delete", @@ -189,6 +192,7 @@ export const BlockMenu = (props: Props) => { } }, }, + ...getNodeOptions(editor), ]; if (!isOpen) {