feat: add tab shortcut function to the default editor table (#5784)

#### 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 快捷键切换单元格的功能
```
pull/5705/head^2
Takagi 2024-04-26 18:00:10 +08:00 committed by GitHub
parent c0de807b9e
commit cb6836aa8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 188 additions and 2 deletions

View File

@ -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<ExtensionOptions & TableOptions>({
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;
},
};
},

View File

@ -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);
};