fix: place cursor at start of inserted table row/column (#12610)

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-06-07 11:58:48 -04:00
committed by GitHub
parent ea665b80ee
commit bc63aba1d1
3 changed files with 109 additions and 6 deletions
+17
View File
@@ -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",
+88 -2
View File
@@ -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;
+4 -4
View File
@@ -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,