diff --git a/ui/packages/editor/src/extensions/table/index.ts b/ui/packages/editor/src/extensions/table/index.ts index 37d98767d..10bb9f875 100644 --- a/ui/packages/editor/src/extensions/table/index.ts +++ b/ui/packages/editor/src/extensions/table/index.ts @@ -16,6 +16,7 @@ import { type NodeView, type EditorState, type DOMOutputSpec, + TextSelection, } from "@/tiptap/pm"; import TableCell from "./table-cell"; import TableRow from "./table-row"; @@ -38,6 +39,8 @@ import { i18n } from "@/locales"; import type { ExtensionOptions, NodeBubbleMenu } from "@/types"; import { BlockActionSeparator, ToolboxItem } from "@/components"; import { + findNextCell, + findPreviousCell, hasTableBefore, isCellSelection, isTableSelected, @@ -522,6 +525,62 @@ const Table = TiptapTable.extend({ editor.commands.setNodeSelection(cellNodePos.pos); return true; }, + Tab: ({ editor }) => { + const { state } = editor; + if (!isActive(editor.state, Table.name)) { + return false; + } + let nextView = editor.view; + let nextTr = editor.state.tr; + + let nextCell = findNextCell(state); + if (!nextCell) { + // If it is the last cell, create a new line and jump to the first cell of the new line. + editor + .chain() + .addRowAfter() + .command(({ tr, view, state }) => { + nextView = view; + nextTr = tr; + nextCell = findNextCell(state); + return true; + }); + } + if (nextCell) { + nextTr.setSelection( + new TextSelection( + nextTr.doc.resolve(nextCell.start), + nextTr.doc.resolve( + nextCell.start + (nextCell.node?.nodeSize || 0) - 4 + ) + ) + ); + nextTr.scrollIntoView(); + nextView.dispatch(nextTr); + return true; + } + return false; + }, + "Shift-Tab": ({ editor }) => { + const { tr } = editor.state; + if (!isActive(editor.state, Table.name)) { + return false; + } + const previousCell = findPreviousCell(editor.state); + if (previousCell) { + tr.setSelection( + new TextSelection( + tr.doc.resolve(previousCell.start), + tr.doc.resolve( + previousCell.start + (previousCell.node?.nodeSize || 0) - 4 + ) + ) + ); + tr.scrollIntoView(); + editor.view.dispatch(tr); + } + return true; + }, }; }, diff --git a/ui/packages/editor/src/extensions/table/util.ts b/ui/packages/editor/src/extensions/table/util.ts index db9e27cc9..ef9252df1 100644 --- a/ui/packages/editor/src/extensions/table/util.ts +++ b/ui/packages/editor/src/extensions/table/util.ts @@ -1,6 +1,6 @@ import { findParentNode } from "@/tiptap/vue-3"; -import { Node, CellSelection, TableMap } from "@/tiptap/pm"; -import type { EditorState, Selection, Transaction } from "@/tiptap/pm"; +import { Node, CellSelection, TableMap, selectedRect } from "@/tiptap/pm"; +import type { EditorState, Rect, Selection, Transaction } from "@/tiptap/pm"; export const selectTable = (tr: Transaction) => { const table = findTable(tr.selection); @@ -250,3 +250,130 @@ export const hasTableBefore = (editorState: EditorState) => { return true; }; + +export const findNextCell = (state: EditorState) => { + return findAdjacentCell(1)(state); +}; + +export const findPreviousCell = (state: EditorState) => { + return findAdjacentCell(-1)(state); +}; + +export const findAdjacentCell = (dir: number) => (state: EditorState) => { + const selectionPosRect = selectedRect(state); + if (selectionPosRect.table) { + const map = selectionPosRect.map; + // currentPos is the position of the current cell in the table map, which is between two cells. + const selectedCells = map.cellsInRect(selectionPosRect); + // Get the currently selected cell boundary + const rect = nextCell(map)(selectedCells[selectedCells.length - 1], dir); + if (rect) { + const { top, left } = rect; + // Get the pos of the current cell according to the boundary + const nextPos = map.map[top * map.width + left]; + return { + start: nextPos + selectionPosRect.tableStart + 2, + node: selectionPosRect.table.nodeAt(nextPos), + }; + } + return undefined; + } +}; + +export const nextCell = (map: TableMap) => (pos: number, dir: number) => { + function findNextCellPos({ top, left, right, bottom }: Rect) { + const nextCellRect = { + top, + left, + right, + bottom, + }; + if (right + 1 > map.width) { + if (bottom === map.height) { + return undefined; + } + nextCellRect.top++; + nextCellRect.left = 0; + nextCellRect.right = 1; + nextCellRect.bottom++; + } else { + nextCellRect.left++; + nextCellRect.right++; + } + const temporaryPos = + map.map[nextCellRect.top * map.width + nextCellRect.left]; + const temporaryRect = map.findCell(temporaryPos); + if ( + temporaryRect.top != nextCellRect.top || + temporaryRect.left < nextCellRect.left + ) { + return findNextCellPos({ + ...nextCellRect, + right: temporaryRect.right, + }); + } + return temporaryPos; + } + + function findPreviousCellPos({ top, left, right, bottom }: Rect) { + const nextCellRect = { + top, + left, + right, + bottom, + }; + if (left - 1 < 0) { + if (top === 0) { + return undefined; + } + nextCellRect.top--; + nextCellRect.left = map.width - 1; + nextCellRect.right = map.width; + nextCellRect.bottom--; + } else { + nextCellRect.left--; + nextCellRect.right--; + } + const temporaryPos = + map.map[nextCellRect.top * map.width + nextCellRect.left]; + const temporaryRect = map.findCell(temporaryPos); + if (temporaryRect.top != nextCellRect.top) { + return findPreviousCellPos(nextCellRect); + } + return temporaryPos; + } + + function nextCellRectByPos(innerPos: number, innerDir: number) { + // Get the current cell boundary + const { top, left, right, bottom } = map.findCell(innerPos); + if (innerDir == 0) { + return { + top, + left, + right, + bottom, + }; + } + + const nextCellRect = { + top, + left, + right, + bottom, + }; + let nextPos; + if (innerDir > 0) { + nextPos = findNextCellPos(nextCellRect); + innerDir--; + } else { + nextPos = findPreviousCellPos(nextCellRect); + innerDir++; + } + if (!nextPos) { + return undefined; + } + return nextCellRectByPos(nextPos, innerDir); + } + + return nextCellRectByPos(pos, dir); +};