Skip to content

Commit 9b40aa8

Browse files
committed
feat: 新增表格复制行
1 parent 0587fc4 commit 9b40aa8

5 files changed

Lines changed: 152 additions & 0 deletions

File tree

lang/index.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3662,5 +3662,21 @@
36623662
"ru": "",
36633663
"en": "",
36643664
"fr": ""
3665+
},
3666+
"bkw8ix4": {
3667+
"zh-cn": "复制本行",
3668+
"ja": "",
3669+
"ko": "",
3670+
"ru": "",
3671+
"en": "",
3672+
"fr": ""
3673+
},
3674+
"rdvpxj6": {
3675+
"zh-cn": "删除此代码块",
3676+
"ja": "",
3677+
"ko": "",
3678+
"ru": "",
3679+
"en": "",
3680+
"fr": ""
36653681
}
36663682
}

src/core/commands/index.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,75 @@ export function deleteColumn(state: EditorState, dispatch?: (tr: Transaction) =>
466466
return true;
467467
}
468468

469+
/**
470+
* 获取当前行的 Markdown 文本
471+
*/
472+
export function getCurrentRowMarkdown(state: EditorState): string | null {
473+
const ctx = findTableContext(state);
474+
if (!ctx) return null;
475+
476+
const rowNode = ctx.tableNode.child(ctx.rowIndex);
477+
const cells: string[] = [];
478+
479+
rowNode.forEach((cell) => {
480+
cells.push(cell.textContent || "");
481+
});
482+
483+
return `| ${cells.join(" | ")} |`;
484+
}
485+
486+
function parseMarkdownTableRow(text: string): string[] | null {
487+
const trimmed = text.trim();
488+
if (!/^\|.*\|$/u.test(trimmed)) return null;
489+
490+
const inner = trimmed.slice(1, -1);
491+
const cells = inner.split("|").map((cell) => cell.trim());
492+
return cells;
493+
}
494+
495+
/**
496+
* 在当前行后插入 Markdown 表格行
497+
*/
498+
export function insertMarkdownTableRowAfterCurrent(
499+
state: EditorState,
500+
rowMarkdown: string,
501+
dispatch?: (tr: Transaction) => void
502+
): boolean {
503+
const ctx = findTableContext(state);
504+
if (!ctx) return false;
505+
506+
const cells = parseMarkdownTableRow(rowMarkdown);
507+
if (!cells) return false;
508+
509+
const colCount = ctx.tableNode.child(0)?.childCount ?? 0;
510+
if (cells.length !== colCount) return false;
511+
512+
const { table_row, table_header, table_cell, paragraph } = state.schema.nodes;
513+
if (!table_row || !table_header || !table_cell || !paragraph) return false;
514+
515+
const cellType = table_cell || table_header;
516+
const rowNode = table_row.create(
517+
null,
518+
cells.map((cellText) =>
519+
cellType.create(
520+
null,
521+
cellText ? paragraph.create(null, state.schema.text(cellText)) : paragraph.create()
522+
)
523+
)
524+
);
525+
526+
if (dispatch) {
527+
const { $from } = state.selection;
528+
const rowPos = $from.after(ctx.rowDepth);
529+
const tr = state.tr.insert(rowPos, rowNode);
530+
const selectionPos = Math.min(rowPos + 2, tr.doc.content.size);
531+
tr.setSelection(TextSelection.create(tr.doc, selectionPos));
532+
dispatch(tr.scrollIntoView());
533+
}
534+
535+
return true;
536+
}
537+
469538
/**
470539
* 插入数学块
471540
*/
@@ -526,6 +595,8 @@ export const commands = {
526595
addColumnAtEnd,
527596
deleteRow,
528597
deleteColumn,
598+
getCurrentRowMarkdown,
599+
insertMarkdownTableRowAfterCurrent,
529600
insertMathBlock,
530601
insertContainer,
531602
};

src/core/editor.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ import {
7878
addColumnAtEnd,
7979
deleteRow,
8080
deleteColumn,
81+
getCurrentRowMarkdown,
82+
insertMarkdownTableRowAfterCurrent,
8183
} from "./commands";
8284
import { DEFAULT_SHORTCUTS, buildActionCommandMap } from "./keymap";
8385
import type { ShortcutActionId } from "./keymap";
@@ -766,6 +768,16 @@ export class MilkupEditor implements IMilkupEditor {
766768
return false;
767769
}
768770

771+
/**
772+
* 将选区同步到右键点击位置
773+
*/
774+
private syncSelectionToMouseEvent(e: MouseEvent): void {
775+
const pos = this.view.posAtCoords({ left: e.clientX, top: e.clientY });
776+
if (!pos) return;
777+
const tr = this.view.state.tr.setSelection(TextSelection.create(this.view.state.doc, pos.pos));
778+
this.view.dispatch(tr);
779+
}
780+
769781
/**
770782
* 创建右键菜单分隔线
771783
*/
@@ -972,9 +984,23 @@ export class MilkupEditor implements IMilkupEditor {
972984
const inTable = this.isInsideTable(e);
973985

974986
if (inTable) {
987+
this.syncSelectionToMouseEvent(e);
988+
975989
// 表格内右键 — 追加表格操作项
976990
menu.appendChild(this.createContextMenuSeparator());
977991

992+
menu.appendChild(
993+
this.createContextMenuItem("复制本行", false, () => {
994+
const rowMarkdown = getCurrentRowMarkdown(this.view.state);
995+
if (rowMarkdown) {
996+
navigator.clipboard.writeText(rowMarkdown);
997+
}
998+
this.hideContextMenu();
999+
})
1000+
);
1001+
1002+
menu.appendChild(this.createContextMenuSeparator());
1003+
9781004
menu.appendChild(
9791005
this.createContextMenuItem("向上插入行", false, () => {
9801006
this.view.focus();
@@ -1587,6 +1613,15 @@ export class MilkupEditor implements IMilkupEditor {
15871613
const blob = await item.getType("text/plain");
15881614
const text = await blob.text();
15891615
if (text) {
1616+
if (
1617+
insertMarkdownTableRowAfterCurrent(
1618+
this.view.state,
1619+
text,
1620+
this.view.dispatch.bind(this.view)
1621+
)
1622+
) {
1623+
return;
1624+
}
15901625
const tr = this.view.state.tr.insertText(text);
15911626
this.view.dispatch(tr);
15921627
}
@@ -1598,6 +1633,15 @@ export class MilkupEditor implements IMilkupEditor {
15981633
try {
15991634
const text = await navigator.clipboard.readText();
16001635
if (text) {
1636+
if (
1637+
insertMarkdownTableRowAfterCurrent(
1638+
this.view.state,
1639+
text,
1640+
this.view.dispatch.bind(this.view)
1641+
)
1642+
) {
1643+
return;
1644+
}
16011645
const tr = this.view.state.tr.insertText(text);
16021646
this.view.dispatch(tr);
16031647
}

src/core/nodeviews/code-block.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,10 @@ export class CodeBlockView implements NodeView {
11311131
// 移除已存在的右键菜单
11321132
this.hideContextMenu();
11331133

1134+
const isHeaderContextMenu = !!(e.target instanceof HTMLElement
1135+
? e.target.closest(".milkup-code-block-header")
1136+
: null);
1137+
11341138
const menu = document.createElement("div");
11351139
menu.className = "milkup-context-menu";
11361140

@@ -1202,6 +1206,18 @@ export class CodeBlockView implements NodeView {
12021206
});
12031207
menu.appendChild(copyBlockItem);
12041208

1209+
if (isHeaderContextMenu) {
1210+
const separator = document.createElement("div");
1211+
separator.className = "milkup-context-menu-separator";
1212+
menu.appendChild(separator);
1213+
1214+
const deleteBlockItem = this.createContextMenuItem("删除此代码块", false, () => {
1215+
this.deleteCodeBlock();
1216+
this.hideContextMenu();
1217+
});
1218+
menu.appendChild(deleteBlockItem);
1219+
}
1220+
12051221
// 定位菜单
12061222
menu.style.left = `${e.clientX}px`;
12071223
menu.style.top = `${e.clientY}px`;

src/core/plugins/paste.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Node, Schema, Slice, Fragment } from "prosemirror-model";
99
import { MarkdownParser } from "../parser";
1010
import { milkupSchema } from "../schema";
1111
import { decorationPluginKey } from "../decorations";
12+
import { insertMarkdownTableRowAfterCurrent } from "../commands";
1213

1314
/** 插件 Key */
1415
export const pastePluginKey = new PluginKey("milkup-paste");
@@ -113,6 +114,10 @@ export function createPastePlugin(config: PastePluginConfig = {}): Plugin {
113114
return false; // 让默认处理器插入纯文本
114115
}
115116

117+
if (insertMarkdownTableRowAfterCurrent(view.state, text, view.dispatch.bind(view))) {
118+
return true;
119+
}
120+
116121
// 检查是否包含 Markdown 语法
117122
if (!containsMarkdownSyntax(text)) {
118123
// 检查是否有外部 HTML(非编辑器内部复制)

0 commit comments

Comments
 (0)