mirror of https://github.com/halo-dev/halo
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
parent
c0de807b9e
commit
cb6836aa8c
|
@ -16,6 +16,7 @@ import {
|
||||||
type NodeView,
|
type NodeView,
|
||||||
type EditorState,
|
type EditorState,
|
||||||
type DOMOutputSpec,
|
type DOMOutputSpec,
|
||||||
|
TextSelection,
|
||||||
} from "@/tiptap/pm";
|
} from "@/tiptap/pm";
|
||||||
import TableCell from "./table-cell";
|
import TableCell from "./table-cell";
|
||||||
import TableRow from "./table-row";
|
import TableRow from "./table-row";
|
||||||
|
@ -38,6 +39,8 @@ import { i18n } from "@/locales";
|
||||||
import type { ExtensionOptions, NodeBubbleMenu } from "@/types";
|
import type { ExtensionOptions, NodeBubbleMenu } from "@/types";
|
||||||
import { BlockActionSeparator, ToolboxItem } from "@/components";
|
import { BlockActionSeparator, ToolboxItem } from "@/components";
|
||||||
import {
|
import {
|
||||||
|
findNextCell,
|
||||||
|
findPreviousCell,
|
||||||
hasTableBefore,
|
hasTableBefore,
|
||||||
isCellSelection,
|
isCellSelection,
|
||||||
isTableSelected,
|
isTableSelected,
|
||||||
|
@ -522,6 +525,62 @@ const Table = TiptapTable.extend<ExtensionOptions & TableOptions>({
|
||||||
editor.commands.setNodeSelection(cellNodePos.pos);
|
editor.commands.setNodeSelection(cellNodePos.pos);
|
||||||
return true;
|
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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { findParentNode } from "@/tiptap/vue-3";
|
import { findParentNode } from "@/tiptap/vue-3";
|
||||||
import { Node, CellSelection, TableMap } from "@/tiptap/pm";
|
import { Node, CellSelection, TableMap, selectedRect } from "@/tiptap/pm";
|
||||||
import type { EditorState, Selection, Transaction } from "@/tiptap/pm";
|
import type { EditorState, Rect, Selection, Transaction } from "@/tiptap/pm";
|
||||||
|
|
||||||
export const selectTable = (tr: Transaction) => {
|
export const selectTable = (tr: Transaction) => {
|
||||||
const table = findTable(tr.selection);
|
const table = findTable(tr.selection);
|
||||||
|
@ -250,3 +250,130 @@ export const hasTableBefore = (editorState: EditorState) => {
|
||||||
|
|
||||||
return true;
|
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);
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue