Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { findTableCellElement } from '../../coreApi/setDOMSelection/findTableCel
import { isSingleImageInSelection } from './isSingleImageInSelection';
import { normalizePos } from './normalizePos';
import {
getDOMInsertPointRect,
getNodePositionFromEvent,
isCharacterValue,
isElementOfType,
isModifierKey,
Expand Down Expand Up @@ -364,7 +366,9 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
this.handleSelectionInTable(this.getTabKey(rawEvent));
rawEvent.preventDefault();
} else {
win?.requestAnimationFrame(() => this.handleSelectionInTable(key));
win?.requestAnimationFrame(() =>
this.handleSelectionInTable(key, selection.range)
);
}
}
}
Expand Down Expand Up @@ -423,7 +427,8 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
}

private handleSelectionInTable(
key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'TabLeft' | 'TabRight'
key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'TabLeft' | 'TabRight',
rangeBeforeChange?: Range
) {
if (!this.editor || !this.state.tableSelection) {
return;
Expand Down Expand Up @@ -468,11 +473,30 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
}

if (collapsed && td) {
this.setRangeSelectionInTable(
td,
key == Up ? td.childNodes.length : 0,
this.editor
);
const textOffset =
(key == 'ArrowUp' || key == 'ArrowDown') && rangeBeforeChange
? this.getTextOffset(
this.editor,
rangeBeforeChange,
td,
key == 'ArrowUp'
)
: null;
if (textOffset) {
this.setRangeSelectionInTable(
textOffset.node,
textOffset.offset,
this.editor,
false /* selectAll */
);
} else {
this.setRangeSelectionInTable(
td,
0,
this.editor,
false /* selectAll */
);
}
} else if (!td && (lastCo.row == -1 || lastCo.row <= parsedTable.length)) {
this.selectBeforeOrAfterElement(
this.editor,
Expand Down Expand Up @@ -545,13 +569,35 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
}
}

private getTextOffset(editor: IEditor, range: Range, td: HTMLElement, isKeyUp: boolean) {
const doc = editor.getDocument();
const cursorRect = range
? getDOMInsertPointRect(doc, {
node: range.startContainer,
offset: range.startOffset,
})
: undefined;
const rect = td?.getBoundingClientRect();
const textOffset =
cursorRect && rect
? getNodePositionFromEvent(
doc,
editor.getDOMHelper(),
cursorRect.left,
isKeyUp ? rect.top - 1 : rect.top + 1
)
: null;
return textOffset;
}

private setRangeSelectionInTable(
cell: Node,
nodeOffset: number,
editor: IEditor,
selectAll?: boolean
) {
const range = editor.getDocument().createRange();
const doc = editor.getDocument();
const range = doc.createRange();
if (selectAll && cell.firstChild && cell.lastChild) {
const cellStart = cell.firstChild;
const cellEnd = cell.lastChild;
Expand All @@ -569,7 +615,6 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
} else {
// Get deepest editable position in the cell
const { node, offset } = normalizePos(cell, nodeOffset);

range.setStart(node, offset);
range.collapse(true /* toStart */);
Comment on lines 617 to 619
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setRangeSelectionInTable now sometimes treats nodeOffset as a text offset (setStart(node, nodeOffset)), but it still calls normalizePos(cell, nodeOffset) first. Since normalizePos interprets the offset relative to the current node (text offset for text nodes, child index for element nodes), passing a text offset when cell is an element can cause it to walk down the “lastChild” path and return an unrelated text node (e.g., the last paragraph in the cell). This can place the caret in the wrong location for cells with multiple child elements/paragraphs. Consider separating “DOM child offset” vs “text offset” semantics: compute the target leaf node independently (e.g., first editable text node in the cell), then clamp/apply textOffset only when you have a text node.

Copilot uses AI. Check for mistakes.
}
Expand Down
Loading
Loading