From bc63aba1d11b081c1f3a22e4e7b9dc5282d0d1e0 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 7 Jun 2026 11:58:48 -0400 Subject: [PATCH] fix: place cursor at start of inserted table row/column (#12610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: place cursor at start of inserted table row/column When using Insert before for a table row or column, the selection was collapsed onto the mapped previous selection — landing at the bottom of the shifted neighbouring column rather than in the newly inserted cell. Move the cursor to the start of the first cell of the inserted row/column instead. * feat: Inline editor menu (#12611) * wip * Mobile support * Address review feedback on inline menu - Mark selection-restore transaction as not added to history - Only open desktop inline menu when an anchor is available Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 * fix: place cursor at start of inserted table row/column When using Insert before for a table row or column, the selection was collapsed onto the mapped previous selection — landing at the bottom of the shifted neighbouring column rather than in the newly inserted cell. Move the cursor to the start of the first cell of the inserted row/column instead. * Add handling for After variants Add lint rule --------- Co-authored-by: Claude --- .oxlintrc.json | 17 +++++++ shared/editor/commands/table.ts | 90 ++++++++++++++++++++++++++++++++- shared/editor/nodes/Table.ts | 8 +-- 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 6be149f106..b1ec802e4e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -73,6 +73,23 @@ "eqeqeq": "error", "curly": "error", "no-console": "error", + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "prosemirror-tables", + "importNames": [ + "addRowBefore", + "addRowAfter", + "addColumnBefore", + "addColumnAfter" + ], + "message": "Use the wrappers from shared/editor/commands/table instead, which respect the target index and place the cursor in the inserted cell." + } + ] + } + ], "no-unused-expressions": "error", "arrow-body-style": ["error", "as-needed"], "react/react-in-jsx-scope": "off", diff --git a/shared/editor/commands/table.ts b/shared/editor/commands/table.ts index daba099d26..68e571c420 100644 --- a/shared/editor/commands/table.ts +++ b/shared/editor/commands/table.ts @@ -65,6 +65,38 @@ function restoreColumnSelection( } } +/** + * A command that places a text cursor at the start of the cell at the given row + * and column index within the table that begins at the given position. Used + * after inserting a row or column so that the selection lands inside the newly + * inserted cell rather than the shifted neighbouring one. + * + * @param tableStart The position inside the table (after the table node). + * @param rowIndex The row index of the target cell. + * @param columnIndex The column index of the target cell. + * @returns The command. + */ +function setCursorInCell( + tableStart: number, + rowIndex: number, + columnIndex: number +): Command { + return (state, dispatch) => { + const table = state.doc.nodeAt(tableStart - 1); + if (!table) { + return false; + } + const map = TableMap.get(table); + if (rowIndex >= map.height || columnIndex >= map.width) { + return false; + } + const pos = map.positionAt(rowIndex, columnIndex, table); + const $pos = state.doc.resolve(tableStart + pos + 1); + dispatch?.(state.tr.setSelection(TextSelection.near($pos))); + return true; + }; +} + export function createTable({ rowsCount, colsCount, @@ -522,7 +554,7 @@ export function addRowBefore({ index }: { index?: number }): Command { (s, d) => !!d?.(addRowWithAlignment(s.tr, rect, position, copyFromRow, s)), headerSpecialCase ? toggleHeader("row") : undefined, - collapseSelection() + setCursorInCell(rect.tableStart, position, 0) )(state, dispatch); return true; @@ -588,7 +620,61 @@ export function addColumnBefore({ index }: { index?: number }): Command { headerSpecialCase ? toggleHeader("column") : undefined, (s, d) => !!d?.(addColumn(s.tr, rect, position)), headerSpecialCase ? toggleHeader("column") : undefined, - collapseSelection() + setCursorInCell(rect.tableStart, 0, position) + )(state, dispatch); + + return true; + }; +} + +/** + * A command that adds a row after the given index (or the current selection), + * copying alignment from the row above and placing the cursor in the new row. + * + * @param index The index of the row to add after, if undefined the current selection is used + * @returns The command + */ +export function addRowAfter({ index }: { index?: number }): Command { + return (state, dispatch) => { + if (!isInTable(state)) { + return false; + } + + const rect = selectedRect(state); + const position = index !== undefined ? index + 1 : rect.bottom; + + // Copy alignment from the row above the insertion point. + const copyFromRow = position - 1; + + chainTransactions( + (s, d) => + !!d?.(addRowWithAlignment(s.tr, rect, position, copyFromRow, s)), + setCursorInCell(rect.tableStart, position, 0) + )(state, dispatch); + + return true; + }; +} + +/** + * A command that adds a column after the given index (or the current selection), + * placing the cursor in the new column. + * + * @param index The index of the column to add after, if undefined the current selection is used + * @returns The command + */ +export function addColumnAfter({ index }: { index?: number }): Command { + return (state, dispatch) => { + if (!isInTable(state)) { + return false; + } + + const rect = selectedRect(state); + const position = index !== undefined ? index + 1 : rect.right; + + chainTransactions( + (s, d) => !!d?.(addColumn(s.tr, rect, position)), + setCursorInCell(rect.tableStart, 0, position) )(state, dispatch); return true; diff --git a/shared/editor/nodes/Table.ts b/shared/editor/nodes/Table.ts index 38418734c0..d850f6512c 100644 --- a/shared/editor/nodes/Table.ts +++ b/shared/editor/nodes/Table.ts @@ -3,8 +3,6 @@ import { InputRule } from "prosemirror-inputrules"; import type { NodeSpec, Node as ProsemirrorNode } from "prosemirror-model"; import { TextSelection } from "prosemirror-state"; import { - addColumnAfter, - addRowAfter, columnResizing, deleteColumn, deleteRow, @@ -17,7 +15,9 @@ import { } from "prosemirror-tables"; import { addRowBefore, + addRowAfter, addColumnBefore, + addColumnAfter, addRowAndMoveSelection, setColumnAttr, createTable, @@ -92,10 +92,10 @@ export default class Table extends Node { setTableAttr, sortTable, addColumnBefore, - addColumnAfter: () => addColumnAfter, + addColumnAfter, deleteColumn: () => deleteColumn, addRowBefore, - addRowAfter: () => addRowAfter, + addRowAfter, moveTableRow, moveTableColumn, deleteRow: () => deleteRow,