From a414ae23f5f039181e71f4ff266d0928cf388a62 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 26 Jun 2025 17:31:29 +0530 Subject: [PATCH 01/11] refactor: tables width and selection UI --- .../src/core/extensions/table/table-cell.ts | 7 + .../src/core/extensions/table/table/index.ts | 2 + .../table/plugins/selection-outline.plugin.ts | 150 +++++++++++++++++ .../extensions/table/table/table-view.tsx | 2 +- .../src/core/extensions/table/table/table.ts | 18 +- .../table/table/utilities/create-table.ts | 22 ++- .../src/core/helpers/editor-commands.ts | 5 +- packages/editor/src/styles/table.css | 156 +++++++++--------- packages/editor/src/styles/variables.css | 15 +- 9 files changed, 273 insertions(+), 104 deletions(-) create mode 100644 packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts diff --git a/packages/editor/src/core/extensions/table/table-cell.ts b/packages/editor/src/core/extensions/table/table-cell.ts index 2ba06845a6c..53e3ad6c11f 100644 --- a/packages/editor/src/core/extensions/table/table-cell.ts +++ b/packages/editor/src/core/extensions/table/table-cell.ts @@ -1,6 +1,9 @@ import { mergeAttributes, Node } from "@tiptap/core"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; +// local imports +import { TableCellSelectionOutlinePlugin } from "./table/plugins/selection-outline.plugin"; + export interface TableCellOptions { HTMLAttributes: Record; } @@ -46,6 +49,10 @@ export const TableCell = Node.create({ isolating: true, + addProseMirrorPlugins() { + return [TableCellSelectionOutlinePlugin(this.editor)]; + }, + parseHTML() { return [{ tag: "td" }]; }, diff --git a/packages/editor/src/core/extensions/table/table/index.ts b/packages/editor/src/core/extensions/table/table/index.ts index 8efc4312099..328422b73d4 100644 --- a/packages/editor/src/core/extensions/table/table/index.ts +++ b/packages/editor/src/core/extensions/table/table/index.ts @@ -1 +1,3 @@ export { Table } from "./table"; + +export const DEFAULT_COLUMN_WIDTH = 150; diff --git a/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts b/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts new file mode 100644 index 00000000000..06685bed394 --- /dev/null +++ b/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts @@ -0,0 +1,150 @@ +import { findParentNode, type Editor } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { CellSelection, TableMap } from "@tiptap/pm/tables"; +import { Decoration, DecorationSet } from "@tiptap/pm/view"; +import type { CSSProperties } from "react"; + +const getTableCellBorderSelectionStatus = ( + cell: number, + selection: number[], + tableMap: TableMap +): { top: boolean; bottom: boolean; left: boolean; right: boolean } => { + const { width, height } = tableMap; + const cellIndex = tableMap.map.indexOf(cell); + + const rect = tableMap.findCell(cell); + const cellW = rect.right - rect.left; + const cellH = rect.bottom - rect.top; + + const testRight = cellW; + const testBottom = width * cellH; + + const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined; + const bottomCell = cellIndex < width * height - testBottom ? tableMap.map[cellIndex + testBottom] : undefined; + const leftCell = cellIndex % width > 0 ? tableMap.map[cellIndex - 1] : undefined; + const rightCell = cellIndex % width < width - testRight ? tableMap.map[cellIndex + testRight] : undefined; + + return { + top: topCell === undefined || !selection.includes(topCell), + bottom: bottomCell === undefined || !selection.includes(bottomCell), + left: leftCell === undefined || !selection.includes(leftCell), + right: rightCell === undefined || !selection.includes(rightCell), + }; +}; + +const createBorderDiv = (side: "top" | "bottom" | "left" | "right"): HTMLElement => { + const div = document.createElement("div"); + + Object.assign(div.style, { + position: "absolute", + backgroundColor: "rgb(var(--color-primary-100))", + pointerEvents: "none", + } satisfies CSSProperties); + + switch (side) { + case "top": + Object.assign(div.style, { + top: "-1px", + left: "-1px", + height: "2px", + width: "calc(100% + 2px)", + } satisfies CSSProperties); + break; + case "bottom": + Object.assign(div.style, { + bottom: "-1px", + left: "-1px", + height: "2px", + width: "calc(100% + 2px)", + } satisfies CSSProperties); + break; + case "left": + Object.assign(div.style, { + top: "-1px", + left: "-1px", + width: "2px", + height: "calc(100% + 2px)", + } satisfies CSSProperties); + break; + case "right": + Object.assign(div.style, { + top: "-1px", + right: "-1px", + width: "2px", + height: "calc(100% + 2px)", + } satisfies CSSProperties); + break; + } + + return div; +}; + +type TableCellSelectionOutlinePluginState = { + decorations?: DecorationSet; +}; + +const SELECTION_OUTLINE_PLUGIN_KEY = new PluginKey("selection"); + +export const TableCellSelectionOutlinePlugin = (editor: Editor): Plugin => + new Plugin({ + key: SELECTION_OUTLINE_PLUGIN_KEY, + state: { + init: () => ({}), + apply(tr, prev, oldState, newState) { + if (!editor.isEditable) return {}; + const table = findParentNode((node) => node.type.spec.tableRole === "table")(newState.selection); + const hasDocChanged = tr.docChanged || !newState.selection.eq(oldState.selection); + if (!table || !hasDocChanged) { + return table === undefined ? {} : prev; + } + + const { selection } = newState; + if (!(selection instanceof CellSelection)) return {}; + + const decorations: Decoration[] = []; + const tableMap = TableMap.get(table.node); + const selected: number[] = []; + + selection.forEachCell((_node, pos) => { + const start = pos - table.pos - 1; + selected.push(start); + }); + + selection.forEachCell((node, pos) => { + const start = pos - table.pos - 1; + const borders = getTableCellBorderSelectionStatus(start, selected, tableMap); + + // Add container div to make cell position relative + const containerDiv = document.createElement("div"); + containerDiv.style.position = "absolute"; + containerDiv.style.height = "100%"; + containerDiv.style.width = "100%"; + containerDiv.style.top = "0"; + containerDiv.style.left = "0"; + + // Add border divs for each side that needs a border + if (borders.top) containerDiv.appendChild(createBorderDiv("top")); + if (borders.bottom) containerDiv.appendChild(createBorderDiv("bottom")); + if (borders.left) containerDiv.appendChild(createBorderDiv("left")); + if (borders.right) containerDiv.appendChild(createBorderDiv("right")); + + // Use widget decoration to insert the container div + decorations.push( + Decoration.widget(pos + 1, containerDiv, { + side: -1, + ignoreSelection: true, + }) + ); + }); + + return { + decorations: DecorationSet.create(newState.doc, decorations), + }; + }, + }, + props: { + decorations(state) { + return SELECTION_OUTLINE_PLUGIN_KEY.getState(state).decorations; + }, + }, + }); diff --git a/packages/editor/src/core/extensions/table/table/table-view.tsx b/packages/editor/src/core/extensions/table/table/table-view.tsx index f78d964ed49..c3466ba59a1 100644 --- a/packages/editor/src/core/extensions/table/table/table-view.tsx +++ b/packages/editor/src/core/extensions/table/table/table-view.tsx @@ -387,7 +387,7 @@ export class TableView implements NodeView { this.root = h( "div", { - className: "table-wrapper horizontal-scrollbar scrollbar-md controls--disabled", + className: "table-wrapper editor-full-width-block horizontal-scrollbar scrollbar-sm controls--disabled", }, this.controls, this.table diff --git a/packages/editor/src/core/extensions/table/table/table.ts b/packages/editor/src/core/extensions/table/table/table.ts index 4810706b395..82a44c20693 100644 --- a/packages/editor/src/core/extensions/table/table/table.ts +++ b/packages/editor/src/core/extensions/table/table/table.ts @@ -29,6 +29,7 @@ import { createTable } from "./utilities/create-table"; import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected"; import { insertLineAboveTableAction } from "./utilities/insert-line-above-table-action"; import { insertLineBelowTableAction } from "./utilities/insert-line-below-table-action"; +import { DEFAULT_COLUMN_WIDTH } from "."; export interface TableOptions { HTMLAttributes: Record; @@ -42,12 +43,7 @@ export interface TableOptions { declare module "@tiptap/core" { interface Commands { [CORE_EXTENSIONS.TABLE]: { - insertTable: (options?: { - rows?: number; - cols?: number; - withHeaderRow?: boolean; - columnWidth?: number; - }) => ReturnType; + insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType; addColumnBefore: () => ReturnType; addColumnAfter: () => ReturnType; deleteColumn: () => ReturnType; @@ -116,9 +112,15 @@ export const Table = Node.create({ addCommands() { return { insertTable: - ({ rows = 3, cols = 3, withHeaderRow = false, columnWidth = 150 } = {}) => + ({ rows = 3, cols = 3, withHeaderRow = false } = {}) => ({ tr, dispatch, editor }) => { - const node = createTable(editor.schema, rows, cols, withHeaderRow, undefined, columnWidth); + const node = createTable({ + schema: editor.schema, + rowsCount: rows, + colsCount: cols, + withHeaderRow, + columnWidth: DEFAULT_COLUMN_WIDTH, + }); if (dispatch) { const offset = tr.selection.anchor + 1; diff --git a/packages/editor/src/core/extensions/table/table/utilities/create-table.ts b/packages/editor/src/core/extensions/table/table/utilities/create-table.ts index 5e9c4ba2451..0ba2757db7b 100644 --- a/packages/editor/src/core/extensions/table/table/utilities/create-table.ts +++ b/packages/editor/src/core/extensions/table/table/utilities/create-table.ts @@ -3,14 +3,18 @@ import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model"; import { createCell } from "@/extensions/table/table/utilities/create-cell"; import { getTableNodeTypes } from "@/extensions/table/table/utilities/get-table-node-types"; -export function createTable( - schema: Schema, - rowsCount: number, - colsCount: number, - withHeaderRow: boolean, - cellContent?: Fragment | ProsemirrorNode | Array, - columnWidth: number = 100 -): ProsemirrorNode { +type Props = { + schema: Schema; + rowsCount: number; + colsCount: number; + withHeaderRow: boolean; + cellContent?: Fragment | ProsemirrorNode | Array; + columnWidth: number; +}; + +export const createTable = (props: Props): ProsemirrorNode => { + const { schema, rowsCount, colsCount, withHeaderRow, cellContent, columnWidth } = props; + const types = getTableNodeTypes(schema); const headerCells: ProsemirrorNode[] = []; const cells: ProsemirrorNode[] = []; @@ -38,4 +42,4 @@ export function createTable( } return types.table.createChecked(null, rows); -} +}; diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index 415a42bb3b4..43543d57539 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -109,9 +109,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => { } } } - if (range) - editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run(); - else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run(); + if (range) editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3 }).run(); + else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3 }).run(); }; export const insertImage = ({ diff --git a/packages/editor/src/styles/table.css b/packages/editor/src/styles/table.css index b4a1ab82071..d79cb6d397b 100644 --- a/packages/editor/src/styles/table.css +++ b/packages/editor/src/styles/table.css @@ -1,48 +1,44 @@ .table-wrapper { overflow-x: auto; - width: fit-content; - max-width: 100%; -} -.table-wrapper table { - border-collapse: collapse; - table-layout: fixed; - margin: 0.5rem 0 1rem 0; - border: 1px solid rgba(var(--color-border-200)); - width: 100%; -} + table { + border-collapse: collapse; + table-layout: fixed; + margin: 0.5rem 0 1rem 0; + border: 1px solid rgba(var(--color-border-200)); + width: 100%; -.table-wrapper table td, -.table-wrapper table th { - min-width: 1em; - border: 1px solid rgba(var(--color-border-200)); - padding: 7px 10px; - vertical-align: top; - box-sizing: border-box; - position: relative; - transition: background-color 0.3s ease; + td, + th { + min-width: 1em; + border: 1px solid rgba(var(--color-border-300)); + padding: 7px 10px; + vertical-align: top; + box-sizing: border-box; + position: relative; + transition: background-color 0.3s ease; - > * { - margin-bottom: 0; - } -} + > * { + margin-bottom: 0; + } -.table-wrapper table { - th { - font-weight: 500; - text-align: left; - } + .ProseMirror-widget + * { + padding-top: 0 !important; + } + } - tr[background="none"], - tr:not([background]) { th { - background-color: rgba(var(--color-background-90)); + font-weight: 500; + text-align: left; } - } -} -.table-wrapper table .selectedCell { - outline: 0.5px solid rgba(var(--color-primary-100)); + tr[background="none"], + tr:not([background]) { + th { + background-color: rgba(var(--color-background-90)); + } + } + } } /* table dropdown */ @@ -59,57 +55,57 @@ .table-wrapper .table-controls { position: absolute; -} -.table-wrapper .table-controls .columns-control, -.table-wrapper .table-controls .rows-control { - transition: opacity ease-in 100ms; - position: absolute; - z-index: 5; - display: flex; - justify-content: center; - align-items: center; -} + .columns-control, + .rows-control { + transition: opacity ease-in 100ms; + position: absolute; + z-index: 5; + display: flex; + justify-content: center; + align-items: center; + } -.table-wrapper .table-controls .columns-control { - height: 20px; - transform: translateY(-50%); -} + .columns-control { + height: 20px; + transform: translateY(-50%); -.table-wrapper .table-controls .columns-control .columns-control-div { - color: white; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); - width: 30px; - height: 15px; -} + .columns-control-div { + color: white; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); + width: 30px; + height: 15px; + } + } -.table-wrapper .table-controls .rows-control { - width: 20px; - transform: translateX(-50%); - left: -8px; -} + .rows-control { + width: 20px; + transform: translateX(-50%); + left: -8px; -.table-wrapper .table-controls .rows-control .rows-control-div { - color: white; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); - height: 30px; - width: 15px; -} + .rows-control-div { + color: white; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); + height: 30px; + width: 15px; + } + } -.table-wrapper .table-controls .rows-control-div, -.table-wrapper .table-controls .columns-control-div { - background-color: rgba(var(--color-background-80)); - border: 0.5px solid rgba(var(--color-border-200)); - border-radius: 4px; - background-size: 1.25rem; - background-repeat: no-repeat; - background-position: center; - transition: - transform ease-out 100ms, - background-color ease-out 100ms; - outline: none; - box-shadow: rgba(var(--color-shadow-2xs)); - cursor: pointer; + .columns-control-div, + .rows-control-div { + background-color: rgba(var(--color-background-80)); + border: 0.5px solid rgba(var(--color-border-200)); + border-radius: 4px; + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: center; + transition: + transform ease-out 100ms, + background-color ease-out 100ms; + outline: none; + box-shadow: rgba(var(--color-shadow-2xs)); + cursor: pointer; + } } .resize-cursor .table-wrapper .table-controls .rows-control, diff --git a/packages/editor/src/styles/variables.css b/packages/editor/src/styles/variables.css index 6d6e2d9b642..8d4e41a3c49 100644 --- a/packages/editor/src/styles/variables.css +++ b/packages/editor/src/styles/variables.css @@ -179,9 +179,18 @@ } .ProseMirror { - max-width: var(--editor-content-width); - margin: 0 auto; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + & > *:not(.editor-full-width-block) { + max-width: var(--editor-content-width); + margin-left: auto !important; + margin-right: auto !important; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + } + + & > .editor-full-width-block { + max-width: 100%; + padding-inline-start: calc((100% - var(--editor-content-width)) / 2); + padding-inline-end: var(--wide-content-margin); + } } } From fdd4f80576d561f9c6c5efaf55cb1e4acc62c8df Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 26 Jun 2025 18:00:11 +0530 Subject: [PATCH 02/11] fix: drag handle position --- packages/editor/src/core/extensions/side-menu.ts | 2 +- packages/editor/src/core/plugins/drag-handle.ts | 6 +++--- packages/editor/src/styles/drag-drop.css | 5 +++-- packages/editor/src/styles/table.css | 6 ++++++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/core/extensions/side-menu.ts b/packages/editor/src/core/extensions/side-menu.ts index 34e3c45e5f2..ca139f72311 100644 --- a/packages/editor/src/core/extensions/side-menu.ts +++ b/packages/editor/src/core/extensions/side-menu.ts @@ -131,7 +131,7 @@ const SideMenu = (options: SideMenuPluginProps) => { } } - if (node.matches(".table-wrapper")) { + if (node.matches("table")) { rect.top += 8; rect.left -= 8; } diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index 4a534bc4cd1..ae3bced4c3f 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -16,7 +16,7 @@ const generalSelectors = [ "blockquote", "h1.editor-heading-block, h2.editor-heading-block, h3.editor-heading-block, h4.editor-heading-block, h5.editor-heading-block, h6.editor-heading-block", "[data-type=horizontalRule]", - ".table-wrapper", + "table", ".issue-embed", ".image-component", ".image-upload-component", @@ -90,7 +90,7 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => { for (const elem of elements) { // Check for table wrapper first - if (elem.matches(".table-wrapper")) { + if (elem.matches("table")) { return elem; } @@ -99,7 +99,7 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => { } // Skip table cells - if (elem.closest(".table-wrapper")) { + if (elem.closest("table")) { continue; } diff --git a/packages/editor/src/styles/drag-drop.css b/packages/editor/src/styles/drag-drop.css index 7db6ed87554..7b9577cc40e 100644 --- a/packages/editor/src/styles/drag-drop.css +++ b/packages/editor/src/styles/drag-drop.css @@ -35,7 +35,7 @@ } /* end ai handle */ -.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(.node-imageComponent):not(.node-image) { +.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(.node-imageComponent):not(.node-image):not(.table-wrapper) { position: relative; cursor: grab; outline: none !important; @@ -61,7 +61,8 @@ } &.node-imageComponent, - &.node-image { + &.node-image, + &.table-wrapper { --horizontal-offset: 0px; &::after { diff --git a/packages/editor/src/styles/table.css b/packages/editor/src/styles/table.css index d79cb6d397b..f0420a3d3e1 100644 --- a/packages/editor/src/styles/table.css +++ b/packages/editor/src/styles/table.css @@ -39,6 +39,12 @@ } } } + + &.ProseMirror-selectednode { + table { + background-color: rgba(var(--color-primary-100), 0.2); + } + } } /* table dropdown */ From a0d6fa0f1e4c6ffa803236b4f3116e182375b18d Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 27 Jun 2025 00:55:43 +0530 Subject: [PATCH 03/11] refactor: selection decorator logic --- .../table/plugins/selection-outline.plugin.ts | 79 +++---------------- packages/editor/src/styles/table.css | 35 ++++++-- 2 files changed, 39 insertions(+), 75 deletions(-) diff --git a/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts b/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts index 06685bed394..8447ab009c5 100644 --- a/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts +++ b/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts @@ -2,7 +2,6 @@ import { findParentNode, type Editor } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; import { CellSelection, TableMap } from "@tiptap/pm/tables"; import { Decoration, DecorationSet } from "@tiptap/pm/view"; -import type { CSSProperties } from "react"; const getTableCellBorderSelectionStatus = ( cell: number, @@ -32,58 +31,11 @@ const getTableCellBorderSelectionStatus = ( }; }; -const createBorderDiv = (side: "top" | "bottom" | "left" | "right"): HTMLElement => { - const div = document.createElement("div"); - - Object.assign(div.style, { - position: "absolute", - backgroundColor: "rgb(var(--color-primary-100))", - pointerEvents: "none", - } satisfies CSSProperties); - - switch (side) { - case "top": - Object.assign(div.style, { - top: "-1px", - left: "-1px", - height: "2px", - width: "calc(100% + 2px)", - } satisfies CSSProperties); - break; - case "bottom": - Object.assign(div.style, { - bottom: "-1px", - left: "-1px", - height: "2px", - width: "calc(100% + 2px)", - } satisfies CSSProperties); - break; - case "left": - Object.assign(div.style, { - top: "-1px", - left: "-1px", - width: "2px", - height: "calc(100% + 2px)", - } satisfies CSSProperties); - break; - case "right": - Object.assign(div.style, { - top: "-1px", - right: "-1px", - width: "2px", - height: "calc(100% + 2px)", - } satisfies CSSProperties); - break; - } - - return div; -}; - type TableCellSelectionOutlinePluginState = { decorations?: DecorationSet; }; -const SELECTION_OUTLINE_PLUGIN_KEY = new PluginKey("selection"); +const SELECTION_OUTLINE_PLUGIN_KEY = new PluginKey("table-cell-selection-outline"); export const TableCellSelectionOutlinePlugin = (editor: Editor): Plugin => new Plugin({ @@ -114,27 +66,14 @@ export const TableCellSelectionOutlinePlugin = (editor: Editor): Plugin Date: Fri, 27 Jun 2025 14:18:52 +0530 Subject: [PATCH 04/11] refactor: adjacent cells logic --- .../table/plugins/selection-outline.plugin.ts | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts b/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts index 8447ab009c5..2bf9b1489fd 100644 --- a/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts +++ b/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts @@ -3,34 +3,53 @@ import { Plugin, PluginKey } from "@tiptap/pm/state"; import { CellSelection, TableMap } from "@tiptap/pm/tables"; import { Decoration, DecorationSet } from "@tiptap/pm/view"; -const getTableCellBorderSelectionStatus = ( - cell: number, - selection: number[], +const getAdjacentCellPositions = ( + cellStart: number, tableMap: TableMap -): { top: boolean; bottom: boolean; left: boolean; right: boolean } => { +): { top?: number; bottom?: number; left?: number; right?: number } => { const { width, height } = tableMap; - const cellIndex = tableMap.map.indexOf(cell); + const cellIndex = tableMap.map.indexOf(cellStart); - const rect = tableMap.findCell(cell); - const cellW = rect.right - rect.left; - const cellH = rect.bottom - rect.top; + if (cellIndex === -1) return {}; - const testRight = cellW; - const testBottom = width * cellH; - - const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined; - const bottomCell = cellIndex < width * height - testBottom ? tableMap.map[cellIndex + testBottom] : undefined; - const leftCell = cellIndex % width > 0 ? tableMap.map[cellIndex - 1] : undefined; - const rightCell = cellIndex % width < width - testRight ? tableMap.map[cellIndex + testRight] : undefined; + const row = Math.floor(cellIndex / width); + const col = cellIndex % width; return { - top: topCell === undefined || !selection.includes(topCell), - bottom: bottomCell === undefined || !selection.includes(bottomCell), - left: leftCell === undefined || !selection.includes(leftCell), - right: rightCell === undefined || !selection.includes(rightCell), + top: row > 0 ? tableMap.map[(row - 1) * width + col] : undefined, + bottom: row < height - 1 ? tableMap.map[(row + 1) * width + col] : undefined, + left: col > 0 ? tableMap.map[row * width + (col - 1)] : undefined, + right: col < width - 1 ? tableMap.map[row * width + (col + 1)] : undefined, }; }; +const getCellBorderClasses = (cellStart: number, selectedCells: number[], tableMap: TableMap): string[] => { + const adjacent = getAdjacentCellPositions(cellStart, tableMap); + const classes: string[] = []; + + // Add border-right if right cell is not selected or doesn't exist + if (adjacent.right === undefined || !selectedCells.includes(adjacent.right)) { + classes.push("selectedCell-border-right"); + } + + // Add border-left if left cell is not selected or doesn't exist + if (adjacent.left === undefined || !selectedCells.includes(adjacent.left)) { + classes.push("selectedCell-border-left"); + } + + // Add border-top if top cell is not selected or doesn't exist + if (adjacent.top === undefined || !selectedCells.includes(adjacent.top)) { + classes.push("selectedCell-border-top"); + } + + // Add border-bottom if bottom cell is not selected or doesn't exist + if (adjacent.bottom === undefined || !selectedCells.includes(adjacent.bottom)) { + classes.push("selectedCell-border-bottom"); + } + + return classes; +}; + type TableCellSelectionOutlinePluginState = { decorations?: DecorationSet; }; @@ -55,23 +74,18 @@ export const TableCellSelectionOutlinePlugin = (editor: Editor): Plugin { const start = pos - table.pos - 1; - selected.push(start); + selectedCells.push(start); }); + // Then, add decorations with appropriate border classes selection.forEachCell((node, pos) => { const start = pos - table.pos - 1; - const borders = getTableCellBorderSelectionStatus(start, selected, tableMap); - - const classes: string[] = []; - - if (borders.top) classes.push("selectedCell-border-top"); - if (borders.bottom) classes.push("selectedCell-border-bottom"); - if (borders.left) classes.push("selectedCell-border-left"); - if (borders.right) classes.push("selectedCell-border-right"); + const classes = getCellBorderClasses(start, selectedCells, tableMap); decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: classes.join(" ") })); }); From 167e53f74cfe40466aed3e679303d3cf051dd7a8 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 27 Jun 2025 14:27:52 +0530 Subject: [PATCH 05/11] refactor: folder structure --- .../src/core/extensions/table/table-cell.ts | 2 +- .../table/plugins/selection-outline.plugin.ts | 103 ------------------ .../plugins/table-selection-outline/plugin.ts | 58 ++++++++++ .../plugins/table-selection-outline/utils.ts | 75 +++++++++++++ 4 files changed, 134 insertions(+), 104 deletions(-) delete mode 100644 packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts create mode 100644 packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/plugin.ts create mode 100644 packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/utils.ts diff --git a/packages/editor/src/core/extensions/table/table-cell.ts b/packages/editor/src/core/extensions/table/table-cell.ts index 53e3ad6c11f..aad1398de45 100644 --- a/packages/editor/src/core/extensions/table/table-cell.ts +++ b/packages/editor/src/core/extensions/table/table-cell.ts @@ -2,7 +2,7 @@ import { mergeAttributes, Node } from "@tiptap/core"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // local imports -import { TableCellSelectionOutlinePlugin } from "./table/plugins/selection-outline.plugin"; +import { TableCellSelectionOutlinePlugin } from "./table/plugins/table-selection-outline/plugin"; export interface TableCellOptions { HTMLAttributes: Record; diff --git a/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts b/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts deleted file mode 100644 index 2bf9b1489fd..00000000000 --- a/packages/editor/src/core/extensions/table/table/plugins/selection-outline.plugin.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { findParentNode, type Editor } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; -import { CellSelection, TableMap } from "@tiptap/pm/tables"; -import { Decoration, DecorationSet } from "@tiptap/pm/view"; - -const getAdjacentCellPositions = ( - cellStart: number, - tableMap: TableMap -): { top?: number; bottom?: number; left?: number; right?: number } => { - const { width, height } = tableMap; - const cellIndex = tableMap.map.indexOf(cellStart); - - if (cellIndex === -1) return {}; - - const row = Math.floor(cellIndex / width); - const col = cellIndex % width; - - return { - top: row > 0 ? tableMap.map[(row - 1) * width + col] : undefined, - bottom: row < height - 1 ? tableMap.map[(row + 1) * width + col] : undefined, - left: col > 0 ? tableMap.map[row * width + (col - 1)] : undefined, - right: col < width - 1 ? tableMap.map[row * width + (col + 1)] : undefined, - }; -}; - -const getCellBorderClasses = (cellStart: number, selectedCells: number[], tableMap: TableMap): string[] => { - const adjacent = getAdjacentCellPositions(cellStart, tableMap); - const classes: string[] = []; - - // Add border-right if right cell is not selected or doesn't exist - if (adjacent.right === undefined || !selectedCells.includes(adjacent.right)) { - classes.push("selectedCell-border-right"); - } - - // Add border-left if left cell is not selected or doesn't exist - if (adjacent.left === undefined || !selectedCells.includes(adjacent.left)) { - classes.push("selectedCell-border-left"); - } - - // Add border-top if top cell is not selected or doesn't exist - if (adjacent.top === undefined || !selectedCells.includes(adjacent.top)) { - classes.push("selectedCell-border-top"); - } - - // Add border-bottom if bottom cell is not selected or doesn't exist - if (adjacent.bottom === undefined || !selectedCells.includes(adjacent.bottom)) { - classes.push("selectedCell-border-bottom"); - } - - return classes; -}; - -type TableCellSelectionOutlinePluginState = { - decorations?: DecorationSet; -}; - -const SELECTION_OUTLINE_PLUGIN_KEY = new PluginKey("table-cell-selection-outline"); - -export const TableCellSelectionOutlinePlugin = (editor: Editor): Plugin => - new Plugin({ - key: SELECTION_OUTLINE_PLUGIN_KEY, - state: { - init: () => ({}), - apply(tr, prev, oldState, newState) { - if (!editor.isEditable) return {}; - const table = findParentNode((node) => node.type.spec.tableRole === "table")(newState.selection); - const hasDocChanged = tr.docChanged || !newState.selection.eq(oldState.selection); - if (!table || !hasDocChanged) { - return table === undefined ? {} : prev; - } - - const { selection } = newState; - if (!(selection instanceof CellSelection)) return {}; - - const decorations: Decoration[] = []; - const tableMap = TableMap.get(table.node); - const selectedCells: number[] = []; - - // First, collect all selected cell positions - selection.forEachCell((_node, pos) => { - const start = pos - table.pos - 1; - selectedCells.push(start); - }); - - // Then, add decorations with appropriate border classes - selection.forEachCell((node, pos) => { - const start = pos - table.pos - 1; - const classes = getCellBorderClasses(start, selectedCells, tableMap); - - decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: classes.join(" ") })); - }); - - return { - decorations: DecorationSet.create(newState.doc, decorations), - }; - }, - }, - props: { - decorations(state) { - return SELECTION_OUTLINE_PLUGIN_KEY.getState(state).decorations; - }, - }, - }); diff --git a/packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/plugin.ts b/packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/plugin.ts new file mode 100644 index 00000000000..0e88d8c7797 --- /dev/null +++ b/packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/plugin.ts @@ -0,0 +1,58 @@ +import { findParentNode, type Editor } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { CellSelection, TableMap } from "@tiptap/pm/tables"; +import { Decoration, DecorationSet } from "@tiptap/pm/view"; +// local imports +import { getCellBorderClasses } from "./utils"; + +type TableCellSelectionOutlinePluginState = { + decorations?: DecorationSet; +}; + +const TABLE_SELECTION_OUTLINE_PLUGIN_KEY = new PluginKey("table-cell-selection-outline"); + +export const TableCellSelectionOutlinePlugin = (editor: Editor): Plugin => + new Plugin({ + key: TABLE_SELECTION_OUTLINE_PLUGIN_KEY, + state: { + init: () => ({}), + apply(tr, prev, oldState, newState) { + if (!editor.isEditable) return {}; + const table = findParentNode((node) => node.type.spec.tableRole === "table")(newState.selection); + const hasDocChanged = tr.docChanged || !newState.selection.eq(oldState.selection); + if (!table || !hasDocChanged) { + return table === undefined ? {} : prev; + } + + const { selection } = newState; + if (!(selection instanceof CellSelection)) return {}; + + const decorations: Decoration[] = []; + const tableMap = TableMap.get(table.node); + const selectedCells: number[] = []; + + // First, collect all selected cell positions + selection.forEachCell((_node, pos) => { + const start = pos - table.pos - 1; + selectedCells.push(start); + }); + + // Then, add decorations with appropriate border classes + selection.forEachCell((node, pos) => { + const start = pos - table.pos - 1; + const classes = getCellBorderClasses(start, selectedCells, tableMap); + + decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: classes.join(" ") })); + }); + + return { + decorations: DecorationSet.create(newState.doc, decorations), + }; + }, + }, + props: { + decorations(state) { + return TABLE_SELECTION_OUTLINE_PLUGIN_KEY.getState(state).decorations; + }, + }, + }); diff --git a/packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/utils.ts b/packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/utils.ts new file mode 100644 index 00000000000..f4c43e77ee6 --- /dev/null +++ b/packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/utils.ts @@ -0,0 +1,75 @@ +import type { TableMap } from "@tiptap/pm/tables"; + +/** + * Calculates the positions of cells adjacent to a given cell in a table + * @param cellStart - The start position of the current cell in the document + * @param tableMap - ProseMirror's table mapping structure containing cell positions and dimensions + * @returns Object with positions of adjacent cells (undefined if cell doesn't exist at table edge) + */ +const getAdjacentCellPositions = ( + cellStart: number, + tableMap: TableMap +): { top?: number; bottom?: number; left?: number; right?: number } => { + // Extract table dimensions + // width -> number of columns in the table + // height -> number of rows in the table + const { width, height } = tableMap; + + // Find the index of our cell in the flat tableMap.map array + // tableMap.map contains start positions of all cells in row-by-row order + const cellIndex = tableMap.map.indexOf(cellStart); + + // Safety check: if cell position not found in table map, return empty object + if (cellIndex === -1) return {}; + + // Convert flat array index to 2D grid coordinates + // row = which row the cell is in (0-based from top) + // col = which column the cell is in (0-based from left) + const row = Math.floor(cellIndex / width); // Integer division gives row number + const col = cellIndex % width; // Remainder gives column number + + return { + // Top cell: same column, one row up + // Check if we're not in the first row (row > 0) before calculating + top: row > 0 ? tableMap.map[(row - 1) * width + col] : undefined, + + // Bottom cell: same column, one row down + // Check if we're not in the last row (row < height - 1) before calculating + bottom: row < height - 1 ? tableMap.map[(row + 1) * width + col] : undefined, + + // Left cell: same row, one column left + // Check if we're not in the first column (col > 0) before calculating + left: col > 0 ? tableMap.map[row * width + (col - 1)] : undefined, + + // Right cell: same row, one column right + // Check if we're not in the last column (col < width - 1) before calculating + right: col < width - 1 ? tableMap.map[row * width + (col + 1)] : undefined, + }; +}; + +export const getCellBorderClasses = (cellStart: number, selectedCells: number[], tableMap: TableMap): string[] => { + const adjacent = getAdjacentCellPositions(cellStart, tableMap); + const classes: string[] = []; + + // Add border-right if right cell is not selected or doesn't exist + if (adjacent.right === undefined || !selectedCells.includes(adjacent.right)) { + classes.push("selectedCell-border-right"); + } + + // Add border-left if left cell is not selected or doesn't exist + if (adjacent.left === undefined || !selectedCells.includes(adjacent.left)) { + classes.push("selectedCell-border-left"); + } + + // Add border-top if top cell is not selected or doesn't exist + if (adjacent.top === undefined || !selectedCells.includes(adjacent.top)) { + classes.push("selectedCell-border-top"); + } + + // Add border-bottom if bottom cell is not selected or doesn't exist + if (adjacent.bottom === undefined || !selectedCells.includes(adjacent.bottom)) { + classes.push("selectedCell-border-bottom"); + } + + return classes; +}; From 13c7ac8f9c340db7ee610387cf4759464969ea85 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 27 Jun 2025 18:42:25 +0530 Subject: [PATCH 06/11] chore: default column width for new columns --- packages/editor/src/core/extensions/table/table-cell.ts | 3 ++- packages/editor/src/core/extensions/table/table-header.ts | 5 ++++- packages/editor/src/core/extensions/table/table/table.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/core/extensions/table/table-cell.ts b/packages/editor/src/core/extensions/table/table-cell.ts index aad1398de45..fa3516b9b6a 100644 --- a/packages/editor/src/core/extensions/table/table-cell.ts +++ b/packages/editor/src/core/extensions/table/table-cell.ts @@ -2,6 +2,7 @@ import { mergeAttributes, Node } from "@tiptap/core"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // local imports +import { DEFAULT_COLUMN_WIDTH } from "./table"; import { TableCellSelectionOutlinePlugin } from "./table/plugins/table-selection-outline/plugin"; export interface TableCellOptions { @@ -28,7 +29,7 @@ export const TableCell = Node.create({ default: 1, }, colwidth: { - default: null, + default: [DEFAULT_COLUMN_WIDTH], parseHTML: (element) => { const colwidth = element.getAttribute("colwidth"); const value = colwidth ? [parseInt(colwidth, 10)] : null; diff --git a/packages/editor/src/core/extensions/table/table-header.ts b/packages/editor/src/core/extensions/table/table-header.ts index 491889eefae..315ada5ec90 100644 --- a/packages/editor/src/core/extensions/table/table-header.ts +++ b/packages/editor/src/core/extensions/table/table-header.ts @@ -1,6 +1,9 @@ import { mergeAttributes, Node } from "@tiptap/core"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; +// local imports +import { DEFAULT_COLUMN_WIDTH } from "./table"; + export interface TableHeaderOptions { HTMLAttributes: Record; } @@ -25,7 +28,7 @@ export const TableHeader = Node.create({ default: 1, }, colwidth: { - default: null, + default: [DEFAULT_COLUMN_WIDTH], parseHTML: (element) => { const colwidth = element.getAttribute("colwidth"); const value = colwidth ? [parseInt(colwidth, 10)] : null; diff --git a/packages/editor/src/core/extensions/table/table/table.ts b/packages/editor/src/core/extensions/table/table/table.ts index 82a44c20693..aa2c79aaf3d 100644 --- a/packages/editor/src/core/extensions/table/table/table.ts +++ b/packages/editor/src/core/extensions/table/table/table.ts @@ -77,7 +77,7 @@ declare module "@tiptap/core" { } } -export const Table = Node.create({ +export const Table = Node.create({ name: CORE_EXTENSIONS.TABLE, addOptions() { From e32807d9081c91337ed67516d3e9519b18cc608c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 27 Jun 2025 19:29:57 +0530 Subject: [PATCH 07/11] refactor: plugin location --- .../table/{table => }/plugins/table-selection-outline/plugin.ts | 0 .../table/{table => }/plugins/table-selection-outline/utils.ts | 0 packages/editor/src/core/extensions/table/table-cell.ts | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/editor/src/core/extensions/table/{table => }/plugins/table-selection-outline/plugin.ts (100%) rename packages/editor/src/core/extensions/table/{table => }/plugins/table-selection-outline/utils.ts (100%) diff --git a/packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/plugin.ts b/packages/editor/src/core/extensions/table/plugins/table-selection-outline/plugin.ts similarity index 100% rename from packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/plugin.ts rename to packages/editor/src/core/extensions/table/plugins/table-selection-outline/plugin.ts diff --git a/packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/utils.ts b/packages/editor/src/core/extensions/table/plugins/table-selection-outline/utils.ts similarity index 100% rename from packages/editor/src/core/extensions/table/table/plugins/table-selection-outline/utils.ts rename to packages/editor/src/core/extensions/table/plugins/table-selection-outline/utils.ts diff --git a/packages/editor/src/core/extensions/table/table-cell.ts b/packages/editor/src/core/extensions/table/table-cell.ts index fa3516b9b6a..988fcbb14d2 100644 --- a/packages/editor/src/core/extensions/table/table-cell.ts +++ b/packages/editor/src/core/extensions/table/table-cell.ts @@ -2,8 +2,8 @@ import { mergeAttributes, Node } from "@tiptap/core"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // local imports +import { TableCellSelectionOutlinePlugin } from "./plugins/table-selection-outline/plugin"; import { DEFAULT_COLUMN_WIDTH } from "./table"; -import { TableCellSelectionOutlinePlugin } from "./table/plugins/table-selection-outline/plugin"; export interface TableCellOptions { HTMLAttributes: Record; From 369be93a772071e0c170a2dc751ddf30178e01e8 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 30 Jun 2025 17:41:12 +0530 Subject: [PATCH 08/11] feat: table insert handlers --- .../plugins/table/insert-handlers/plugin.ts | 87 ++++ .../plugins/table/insert-handlers/utils.ts | 426 ++++++++++++++++++ .../selection-outline}/plugin.ts | 0 .../selection-outline}/utils.ts | 0 .../src/core/extensions/table/table-cell.ts | 2 +- .../src/core/extensions/table/table/table.ts | 2 + packages/editor/src/styles/table.css | 63 ++- 7 files changed, 577 insertions(+), 3 deletions(-) create mode 100644 packages/editor/src/core/extensions/table/plugins/table/insert-handlers/plugin.ts create mode 100644 packages/editor/src/core/extensions/table/plugins/table/insert-handlers/utils.ts rename packages/editor/src/core/extensions/table/plugins/{table-selection-outline => table/selection-outline}/plugin.ts (100%) rename packages/editor/src/core/extensions/table/plugins/{table-selection-outline => table/selection-outline}/utils.ts (100%) diff --git a/packages/editor/src/core/extensions/table/plugins/table/insert-handlers/plugin.ts b/packages/editor/src/core/extensions/table/plugins/table/insert-handlers/plugin.ts new file mode 100644 index 00000000000..97cd2d09f7a --- /dev/null +++ b/packages/editor/src/core/extensions/table/plugins/table/insert-handlers/plugin.ts @@ -0,0 +1,87 @@ +import { type Editor } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +// local imports +import { createColumnInsertButton, createRowInsertButton, findAllTables, TableInfo } from "./utils"; + +const TABLE_INSERT_PLUGIN_KEY = new PluginKey("table-insert"); + +export const TableInsertPlugin = (editor: Editor): Plugin => { + const tableMap = new Map(); + + const setupTable = (tableInfo: TableInfo) => { + const { tableElement } = tableInfo; + + // Create and add column button if it doesn't exist + if (!tableInfo.columnButtonElement) { + const columnButton = createColumnInsertButton(editor, tableInfo); + tableElement.appendChild(columnButton); + tableInfo.columnButtonElement = columnButton; + } + + // Create and add row button if it doesn't exist + if (!tableInfo.rowButtonElement) { + const rowButton = createRowInsertButton(editor, tableInfo); + tableElement.appendChild(rowButton); + tableInfo.rowButtonElement = rowButton; + } + + tableMap.set(tableElement, tableInfo); + }; + + const cleanupTable = (tableElement: HTMLElement) => { + const tableInfo = tableMap.get(tableElement); + tableInfo?.columnButtonElement?.remove(); + tableInfo?.rowButtonElement?.remove(); + tableMap.delete(tableElement); + }; + + const updateAllTables = () => { + if (!editor.isEditable) { + // Clean up all tables if editor is not editable + tableMap.forEach((_, tableElement) => { + cleanupTable(tableElement); + }); + return; + } + + const currentTables = findAllTables(editor); + const currentTableElements = new Set(currentTables.map((t) => t.tableElement)); + + // Remove buttons from tables that no longer exist + tableMap.forEach((_, tableElement) => { + if (!currentTableElements.has(tableElement)) { + cleanupTable(tableElement); + } + }); + + // Add buttons to new tables + currentTables.forEach((tableInfo) => { + if (!tableMap.has(tableInfo.tableElement)) { + setupTable(tableInfo); + } + }); + }; + + return new Plugin({ + key: TABLE_INSERT_PLUGIN_KEY, + view() { + setTimeout(updateAllTables, 0); + + return { + update(view, prevState) { + // Update when document changes + if (!prevState.doc.eq(view.state.doc)) { + updateAllTables(); + } + }, + destroy() { + // Clean up all tables + tableMap.forEach((_, tableElement) => { + cleanupTable(tableElement); + }); + tableMap.clear(); + }, + }; + }, + }); +}; diff --git a/packages/editor/src/core/extensions/table/plugins/table/insert-handlers/utils.ts b/packages/editor/src/core/extensions/table/plugins/table/insert-handlers/utils.ts new file mode 100644 index 00000000000..c86e0c666c5 --- /dev/null +++ b/packages/editor/src/core/extensions/table/plugins/table/insert-handlers/utils.ts @@ -0,0 +1,426 @@ +import type { Editor } from "@tiptap/core"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { addColumn, removeColumn, addRow, removeRow, TableMap } from "@tiptap/pm/tables"; + +const addSvg = ` + +`; + +export type TableInfo = { + tableElement: HTMLElement; + tableNode: ProseMirrorNode; + tablePos: number; + columnButtonElement?: HTMLElement; + rowButtonElement?: HTMLElement; +}; + +export const createColumnInsertButton = (editor: Editor, tableInfo: TableInfo): HTMLElement => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "table-column-insert-button"; + + const icon = document.createElement("span"); + icon.innerHTML = addSvg; + button.appendChild(icon); + + let mouseDownX = 0; + let isDragging = false; + let dragStarted = false; + let lastActionX = 0; + const DRAG_THRESHOLD = 5; // pixels to start drag + const ACTION_THRESHOLD = 150; // pixels total distance to trigger action + + const onMouseDown = (e: MouseEvent) => { + if (e.button !== 0) return; // Only left mouse button + + e.preventDefault(); + e.stopPropagation(); + + mouseDownX = e.clientX; + lastActionX = e.clientX; + isDragging = false; + dragStarted = false; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }; + + const onMouseMove = (e: MouseEvent) => { + const deltaX = e.clientX - mouseDownX; + const distance = Math.abs(deltaX); + + // Start dragging if moved more than threshold + if (!isDragging && distance > DRAG_THRESHOLD) { + isDragging = true; + dragStarted = true; + + // Visual feedback + button.classList.add("dragging"); + document.body.style.userSelect = "none"; + } + + if (isDragging) { + const totalDistance = Math.abs(e.clientX - lastActionX); + + // Only trigger action when total distance reaches threshold + if (totalDistance >= ACTION_THRESHOLD) { + // Determine direction based on current movement relative to last action point + const directionFromLastAction = e.clientX - lastActionX; + + // Right direction - add columns + if (directionFromLastAction > 0) { + insertColumnAfterLast(editor, tableInfo); + lastActionX = e.clientX; // Reset action point + } + // Left direction - delete empty columns + else if (directionFromLastAction < 0) { + const deleted = removeLastColumn(editor, tableInfo); + if (deleted) { + lastActionX = e.clientX; // Reset action point + } + } + } + } + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + if (isDragging) { + // Clean up drag state + button.classList.remove("dragging"); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } else if (!dragStarted) { + // Handle as click if no dragging occurred + insertColumnAfterLast(editor, tableInfo); + } + + isDragging = false; + dragStarted = false; + }; + + button.addEventListener("mousedown", onMouseDown); + + // Prevent context menu and text selection + button.addEventListener("contextmenu", (e) => e.preventDefault()); + button.addEventListener("selectstart", (e) => e.preventDefault()); + + return button; +}; + +export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTMLElement => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "table-row-insert-button"; + + const icon = document.createElement("span"); + icon.innerHTML = addSvg; + button.appendChild(icon); + + let mouseDownY = 0; + let isDragging = false; + let dragStarted = false; + let lastActionY = 0; + const DRAG_THRESHOLD = 5; // pixels to start drag + const ACTION_THRESHOLD = 40; // pixels total distance to trigger action + + const onMouseDown = (e: MouseEvent) => { + if (e.button !== 0) return; // Only left mouse button + + e.preventDefault(); + e.stopPropagation(); + + mouseDownY = e.clientY; + lastActionY = e.clientY; + isDragging = false; + dragStarted = false; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }; + + const onMouseMove = (e: MouseEvent) => { + const deltaY = e.clientY - mouseDownY; + const distance = Math.abs(deltaY); + + // Start dragging if moved more than threshold + if (!isDragging && distance > DRAG_THRESHOLD) { + isDragging = true; + dragStarted = true; + + // Visual feedback + button.classList.add("dragging"); + document.body.style.userSelect = "none"; + } + + if (isDragging) { + const totalDistance = Math.abs(e.clientY - lastActionY); + + // Only trigger action when total distance reaches threshold + if (totalDistance >= ACTION_THRESHOLD) { + // Determine direction based on current movement relative to last action point + const directionFromLastAction = e.clientY - lastActionY; + + // Down direction - add rows + if (directionFromLastAction > 0) { + insertRowAfterLast(editor, tableInfo); + lastActionY = e.clientY; // Reset action point + } + // Up direction - delete empty rows + else if (directionFromLastAction < 0) { + const deleted = removeLastRow(editor, tableInfo); + if (deleted) { + lastActionY = e.clientY; // Reset action point + } + } + } + } + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + if (isDragging) { + // Clean up drag state + button.classList.remove("dragging"); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } else if (!dragStarted) { + // Handle as click if no dragging occurred + insertRowAfterLast(editor, tableInfo); + } + + isDragging = false; + dragStarted = false; + }; + + button.addEventListener("mousedown", onMouseDown); + + // Prevent context menu and text selection + button.addEventListener("contextmenu", (e) => e.preventDefault()); + button.addEventListener("selectstart", (e) => e.preventDefault()); + + return button; +}; + +export const findAllTables = (editor: Editor): TableInfo[] => { + const tables: TableInfo[] = []; + const tableElements = editor.view.dom.querySelectorAll("table"); + + tableElements.forEach((tableElement) => { + // Find the table's ProseMirror position + let tablePos = -1; + let tableNode: ProseMirrorNode | null = null; + + // Walk through the document to find matching table nodes + editor.state.doc.descendants((node, pos) => { + if (node.type.spec.tableRole === "table") { + const domAtPos = editor.view.domAtPos(pos + 1); + let domTable = domAtPos.node; + + // Navigate to find the table element + while (domTable && domTable.parentNode && domTable.nodeType !== Node.ELEMENT_NODE) { + domTable = domTable.parentNode; + } + + while (domTable && domTable.parentNode && (domTable as HTMLElement).tagName !== "TABLE") { + domTable = domTable.parentNode; + } + + if (domTable === tableElement) { + tablePos = pos; + tableNode = node; + return false; // Stop iteration + } + } + }); + + if (tablePos !== -1 && tableNode) { + tables.push({ + tableElement, + tableNode, + tablePos, + }); + } + }); + + return tables; +}; + +const getCurrentTableInfo = (editor: Editor, tableInfo: TableInfo): TableInfo => { + // Refresh table info to get latest state + const tables = findAllTables(editor); + const updated = tables.find((t) => t.tableElement === tableInfo.tableElement); + return updated || tableInfo; +}; + +// Column functions +const insertColumnAfterLast = (editor: Editor, tableInfo: TableInfo) => { + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const { tableNode, tablePos } = currentTableInfo; + const tableMapData = TableMap.get(tableNode); + const lastColumnIndex = tableMapData.width; + + const tr = editor.state.tr; + const rect = { + map: tableMapData, + tableStart: tablePos, + table: tableNode, + top: 0, + left: 0, + bottom: tableMapData.height - 1, + right: tableMapData.width - 1, + }; + + const newTr = addColumn(tr, rect, lastColumnIndex); + editor.view.dispatch(newTr); +}; + +const removeLastColumn = (editor: Editor, tableInfo: TableInfo): boolean => { + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const { tableNode, tablePos } = currentTableInfo; + const tableMapData = TableMap.get(tableNode); + + // Don't delete if only one column left + if (tableMapData.width <= 1) { + return false; + } + + const lastColumnIndex = tableMapData.width - 1; + + // Check if last column is empty + if (!isColumnEmpty(currentTableInfo, lastColumnIndex)) { + return false; + } + + const tr = editor.state.tr; + const rect = { + map: tableMapData, + tableStart: tablePos, + table: tableNode, + top: 0, + left: 0, + bottom: tableMapData.height - 1, + right: tableMapData.width - 1, + }; + + removeColumn(tr, rect, lastColumnIndex); + editor.view.dispatch(tr); + return true; +}; + +// Helper function to check if a single cell is empty +const isCellEmpty = (cell: ProseMirrorNode | null | undefined): boolean => { + if (!cell || cell.content.size === 0) { + return true; + } + + // Check if cell has any non-empty content + let hasContent = false; + cell.content.forEach((node) => { + if (node.type.name === "paragraph") { + if (node.content.size > 0) { + hasContent = true; + } + } else if (node.content.size > 0 || node.isText) { + hasContent = true; + } + }); + + return !hasContent; +}; + +const isColumnEmpty = (tableInfo: TableInfo, columnIndex: number): boolean => { + const { tableNode } = tableInfo; + const tableMapData = TableMap.get(tableNode); + + // Check each cell in the column + for (let row = 0; row < tableMapData.height; row++) { + const cellIndex = row * tableMapData.width + columnIndex; + const cellPos = tableMapData.map[cellIndex]; + const cell = tableNode.nodeAt(cellPos); + + if (!isCellEmpty(cell)) { + return false; + } + } + return true; +}; + +// Row functions +const insertRowAfterLast = (editor: Editor, tableInfo: TableInfo) => { + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const { tableNode, tablePos } = currentTableInfo; + const tableMapData = TableMap.get(tableNode); + const lastRowIndex = tableMapData.height; + + const tr = editor.state.tr; + const rect = { + map: tableMapData, + tableStart: tablePos, + table: tableNode, + top: 0, + left: 0, + bottom: tableMapData.height - 1, + right: tableMapData.width - 1, + }; + + const newTr = addRow(tr, rect, lastRowIndex); + editor.view.dispatch(newTr); +}; + +const removeLastRow = (editor: Editor, tableInfo: TableInfo): boolean => { + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const { tableNode, tablePos } = currentTableInfo; + const tableMapData = TableMap.get(tableNode); + + // Don't delete if only one row left + if (tableMapData.height <= 1) { + return false; + } + + const lastRowIndex = tableMapData.height - 1; + + // Check if last row is empty + if (!isRowEmpty(currentTableInfo, lastRowIndex)) { + return false; + } + + const tr = editor.state.tr; + const rect = { + map: tableMapData, + tableStart: tablePos, + table: tableNode, + top: 0, + left: 0, + bottom: tableMapData.height - 1, + right: tableMapData.width - 1, + }; + + removeRow(tr, rect, lastRowIndex); + editor.view.dispatch(tr); + return true; +}; + +const isRowEmpty = (tableInfo: TableInfo, rowIndex: number): boolean => { + const { tableNode } = tableInfo; + const tableMapData = TableMap.get(tableNode); + + // Check each cell in the row + for (let col = 0; col < tableMapData.width; col++) { + const cellIndex = rowIndex * tableMapData.width + col; + const cellPos = tableMapData.map[cellIndex]; + const cell = tableNode.nodeAt(cellPos); + + if (!isCellEmpty(cell)) { + return false; + } + } + return true; +}; diff --git a/packages/editor/src/core/extensions/table/plugins/table-selection-outline/plugin.ts b/packages/editor/src/core/extensions/table/plugins/table/selection-outline/plugin.ts similarity index 100% rename from packages/editor/src/core/extensions/table/plugins/table-selection-outline/plugin.ts rename to packages/editor/src/core/extensions/table/plugins/table/selection-outline/plugin.ts diff --git a/packages/editor/src/core/extensions/table/plugins/table-selection-outline/utils.ts b/packages/editor/src/core/extensions/table/plugins/table/selection-outline/utils.ts similarity index 100% rename from packages/editor/src/core/extensions/table/plugins/table-selection-outline/utils.ts rename to packages/editor/src/core/extensions/table/plugins/table/selection-outline/utils.ts diff --git a/packages/editor/src/core/extensions/table/table-cell.ts b/packages/editor/src/core/extensions/table/table-cell.ts index 988fcbb14d2..1e7fa26f5f2 100644 --- a/packages/editor/src/core/extensions/table/table-cell.ts +++ b/packages/editor/src/core/extensions/table/table-cell.ts @@ -2,7 +2,7 @@ import { mergeAttributes, Node } from "@tiptap/core"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // local imports -import { TableCellSelectionOutlinePlugin } from "./plugins/table-selection-outline/plugin"; +import { TableCellSelectionOutlinePlugin } from "./plugins/table/selection-outline/plugin"; import { DEFAULT_COLUMN_WIDTH } from "./table"; export interface TableCellOptions { diff --git a/packages/editor/src/core/extensions/table/table/table.ts b/packages/editor/src/core/extensions/table/table/table.ts index aa2c79aaf3d..6af52460cd7 100644 --- a/packages/editor/src/core/extensions/table/table/table.ts +++ b/packages/editor/src/core/extensions/table/table/table.ts @@ -23,6 +23,7 @@ import { Decoration } from "@tiptap/pm/view"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // local imports +import { TableInsertPlugin } from "../plugins/table/insert-handlers/plugin"; import { tableControls } from "./table-controls"; import { TableView } from "./table-view"; import { createTable } from "./utilities/create-table"; @@ -266,6 +267,7 @@ export const Table = Node.create({ allowTableNodeSelection: this.options.allowTableNodeSelection, }), tableControls(), + TableInsertPlugin(this.editor), ]; if (isResizable) { diff --git a/packages/editor/src/styles/table.css b/packages/editor/src/styles/table.css index 8c0b1786af7..f57b8803af6 100644 --- a/packages/editor/src/styles/table.css +++ b/packages/editor/src/styles/table.css @@ -1,10 +1,12 @@ .table-wrapper { overflow-x: auto; + padding-bottom: 30px; table { + position: relative; border-collapse: collapse; table-layout: fixed; - margin: 0.5rem 0 1rem 0; + margin: 0.5rem 0 0 0; border: 1px solid rgba(var(--color-border-200)); width: 100%; @@ -70,9 +72,66 @@ background-color: rgba(var(--color-primary-100), 0.2); } } + + /* Insert buttons */ + .table-column-insert-button, + .table-row-insert-button { + position: absolute; + background-color: rgba(var(--color-background-90)); + color: rgba(var(--color-text-300)); + border: 1px solid rgba(var(--color-border-200)); + border-radius: 4px; + display: grid; + place-items: center; + opacity: 0; + outline: none; + z-index: 1000; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(var(--color-background-80)); + color: rgba(var(--color-text-100)); + } + + &.dragging { + opacity: 1; + background-color: rgba(var(--color-primary-100), 0.2); + color: rgba(var(--color-text-100)); + } + + svg { + width: 12px; + height: 12px; + } + } + + .table-column-insert-button { + top: 0; + right: -20px; + width: 20px; + height: 100%; + transform: translateX(50%); + } + + .table-row-insert-button { + bottom: -20px; + left: 0; + width: 100%; + height: 20px; + transform: translateY(50%); + } + + /* Show buttons on table hover */ + &:hover { + .table-column-insert-button, + .table-row-insert-button { + opacity: 1; + } + } + /* End insert buttons */ } -/* table dropdown */ +/* Rest of your existing CSS... */ .table-wrapper table .column-resize-handle { position: absolute; right: -1px; From 50e342638fcc37fe53be19c860b69d9ae688a06c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 30 Jun 2025 19:37:19 +0530 Subject: [PATCH 09/11] refactor: css rules --- packages/editor/src/styles/table.css | 124 ++++++++++++++------------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/packages/editor/src/styles/table.css b/packages/editor/src/styles/table.css index f57b8803af6..4a2bda3c8b7 100644 --- a/packages/editor/src/styles/table.css +++ b/packages/editor/src/styles/table.css @@ -67,71 +67,16 @@ } } + /* Selected status */ &.ProseMirror-selectednode { table { background-color: rgba(var(--color-primary-100), 0.2); } } - - /* Insert buttons */ - .table-column-insert-button, - .table-row-insert-button { - position: absolute; - background-color: rgba(var(--color-background-90)); - color: rgba(var(--color-text-300)); - border: 1px solid rgba(var(--color-border-200)); - border-radius: 4px; - display: grid; - place-items: center; - opacity: 0; - outline: none; - z-index: 1000; - transition: all 0.2s ease; - - &:hover { - background-color: rgba(var(--color-background-80)); - color: rgba(var(--color-text-100)); - } - - &.dragging { - opacity: 1; - background-color: rgba(var(--color-primary-100), 0.2); - color: rgba(var(--color-text-100)); - } - - svg { - width: 12px; - height: 12px; - } - } - - .table-column-insert-button { - top: 0; - right: -20px; - width: 20px; - height: 100%; - transform: translateX(50%); - } - - .table-row-insert-button { - bottom: -20px; - left: 0; - width: 100%; - height: 20px; - transform: translateY(50%); - } - - /* Show buttons on table hover */ - &:hover { - .table-column-insert-button, - .table-row-insert-button { - opacity: 1; - } - } - /* End insert buttons */ + /* End selected status */ } -/* Rest of your existing CSS... */ +/* Column resizer */ .table-wrapper table .column-resize-handle { position: absolute; right: -1px; @@ -142,6 +87,7 @@ background-color: rgba(var(--color-primary-100)); pointer-events: none; } +/* End column resizer */ .table-wrapper .table-controls { position: absolute; @@ -205,3 +151,65 @@ opacity: 0; pointer-events: none; } + +/* Insert buttons */ +.table-wrapper { + .table-column-insert-button, + .table-row-insert-button { + position: absolute; + background-color: rgba(var(--color-background-90)); + color: rgba(var(--color-text-300)); + border: 1px solid rgba(var(--color-border-200)); + border-radius: 4px; + display: grid; + place-items: center; + opacity: 0; + pointer-events: none; + outline: none; + z-index: 1000; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(var(--color-background-80)); + color: rgba(var(--color-text-100)); + } + + &.dragging { + opacity: 1; + pointer-events: auto; + background-color: rgba(var(--color-primary-100), 0.2); + color: rgba(var(--color-text-100)); + } + + svg { + width: 12px; + height: 12px; + } + } + + .table-column-insert-button { + top: 0; + right: -20px; + width: 20px; + height: 100%; + transform: translateX(50%); + } + + .table-row-insert-button { + bottom: -20px; + left: 0; + width: 100%; + height: 20px; + transform: translateY(50%); + } + + /* Show buttons on table hover */ + &:hover { + .table-column-insert-button, + .table-row-insert-button { + opacity: 1; + pointer-events: auto; + } + } +} +/* End insert buttons */ From 9d1d3755d0b5565519963cc21264cc838cf52941 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 30 Jun 2025 19:39:48 +0530 Subject: [PATCH 10/11] refactor: plugins folder structure --- .../table/plugins/{table => }/insert-handlers/plugin.ts | 0 .../table/plugins/{table => }/insert-handlers/utils.ts | 0 .../table/plugins/{table => }/selection-outline/plugin.ts | 0 .../table/plugins/{table => }/selection-outline/utils.ts | 0 packages/editor/src/core/extensions/table/table-cell.ts | 2 +- packages/editor/src/core/extensions/table/table/table.ts | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename packages/editor/src/core/extensions/table/plugins/{table => }/insert-handlers/plugin.ts (100%) rename packages/editor/src/core/extensions/table/plugins/{table => }/insert-handlers/utils.ts (100%) rename packages/editor/src/core/extensions/table/plugins/{table => }/selection-outline/plugin.ts (100%) rename packages/editor/src/core/extensions/table/plugins/{table => }/selection-outline/utils.ts (100%) diff --git a/packages/editor/src/core/extensions/table/plugins/table/insert-handlers/plugin.ts b/packages/editor/src/core/extensions/table/plugins/insert-handlers/plugin.ts similarity index 100% rename from packages/editor/src/core/extensions/table/plugins/table/insert-handlers/plugin.ts rename to packages/editor/src/core/extensions/table/plugins/insert-handlers/plugin.ts diff --git a/packages/editor/src/core/extensions/table/plugins/table/insert-handlers/utils.ts b/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts similarity index 100% rename from packages/editor/src/core/extensions/table/plugins/table/insert-handlers/utils.ts rename to packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts diff --git a/packages/editor/src/core/extensions/table/plugins/table/selection-outline/plugin.ts b/packages/editor/src/core/extensions/table/plugins/selection-outline/plugin.ts similarity index 100% rename from packages/editor/src/core/extensions/table/plugins/table/selection-outline/plugin.ts rename to packages/editor/src/core/extensions/table/plugins/selection-outline/plugin.ts diff --git a/packages/editor/src/core/extensions/table/plugins/table/selection-outline/utils.ts b/packages/editor/src/core/extensions/table/plugins/selection-outline/utils.ts similarity index 100% rename from packages/editor/src/core/extensions/table/plugins/table/selection-outline/utils.ts rename to packages/editor/src/core/extensions/table/plugins/selection-outline/utils.ts diff --git a/packages/editor/src/core/extensions/table/table-cell.ts b/packages/editor/src/core/extensions/table/table-cell.ts index 1e7fa26f5f2..a9c98717bef 100644 --- a/packages/editor/src/core/extensions/table/table-cell.ts +++ b/packages/editor/src/core/extensions/table/table-cell.ts @@ -2,7 +2,7 @@ import { mergeAttributes, Node } from "@tiptap/core"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // local imports -import { TableCellSelectionOutlinePlugin } from "./plugins/table/selection-outline/plugin"; +import { TableCellSelectionOutlinePlugin } from "./plugins/selection-outline/plugin"; import { DEFAULT_COLUMN_WIDTH } from "./table"; export interface TableCellOptions { diff --git a/packages/editor/src/core/extensions/table/table/table.ts b/packages/editor/src/core/extensions/table/table/table.ts index 6af52460cd7..66e54adcdaf 100644 --- a/packages/editor/src/core/extensions/table/table/table.ts +++ b/packages/editor/src/core/extensions/table/table/table.ts @@ -23,7 +23,7 @@ import { Decoration } from "@tiptap/pm/view"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // local imports -import { TableInsertPlugin } from "../plugins/table/insert-handlers/plugin"; +import { TableInsertPlugin } from "../plugins/insert-handlers/plugin"; import { tableControls } from "./table-controls"; import { TableView } from "./table-view"; import { createTable } from "./utilities/create-table"; From ace33f6093f694f77559cd70226894dd3375d926 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 30 Jun 2025 19:59:37 +0530 Subject: [PATCH 11/11] chore: add aria labels --- .../core/extensions/table/plugins/insert-handlers/utils.ts | 4 ++++ packages/editor/src/styles/table.css | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts b/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts index c86e0c666c5..1306e7919a1 100644 --- a/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts +++ b/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts @@ -21,6 +21,8 @@ export const createColumnInsertButton = (editor: Editor, tableInfo: TableInfo): const button = document.createElement("button"); button.type = "button"; button.className = "table-column-insert-button"; + button.title = "Insert columns"; + button.ariaLabel = "Insert columns"; const icon = document.createElement("span"); icon.innerHTML = addSvg; @@ -117,6 +119,8 @@ export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTM const button = document.createElement("button"); button.type = "button"; button.className = "table-row-insert-button"; + button.title = "Insert rows"; + button.ariaLabel = "Insert rows"; const icon = document.createElement("span"); icon.innerHTML = addSvg; diff --git a/packages/editor/src/styles/table.css b/packages/editor/src/styles/table.css index 4a2bda3c8b7..e9556c9fdba 100644 --- a/packages/editor/src/styles/table.css +++ b/packages/editor/src/styles/table.css @@ -24,6 +24,7 @@ margin-bottom: 0; } + /* Selected cell outline */ &.selectedCell { user-select: none; @@ -52,6 +53,7 @@ border-right: 2px solid rgba(var(--color-primary-100)); } } + /* End selected cell outline */ } th {