From cb6836aa8cd3e0d92532f069a911128d9f05a7db Mon Sep 17 00:00:00 2001 From: Takagi <1103069291@qq.com> Date: Fri, 26 Apr 2024 18:00:10 +0800 Subject: [PATCH] feat: add tab shortcut function to the default editor table (#5784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area editor #### What this PR does / why we need it: 为默认编辑器表格增加 `Tab` 与 `Shift-Tab` 切换至上一个单元格或下一个单元格。具体功能如下: 1. 使用 Tab 快捷键从左向右切换至下一个单元格,当光标在最后一个单元格时,使用 Tab 键新建一行并跳转至新一行的第一个单元格。 2. 使用 Shift + Tab 快捷键从右向左来切换至上一个单元格。 #### How to test it? 测试在默认编辑器中 `Tab` 快捷键切换单元格是否生效。 测试在合并单元格等各种表格操作下,切换单元格是否生效。 #### Which issue(s) this PR fixes: Fixes #5771 #### Does this PR introduce a user-facing change? ```release-note 为默认编辑器表格增加 Tab 快捷键切换单元格的功能 ``` --- .../editor/src/extensions/table/index.ts | 59 ++++++++ .../editor/src/extensions/table/util.ts | 131 +++++++++++++++++- 2 files changed, 188 insertions(+), 2 deletions(-) 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); +};