Skip to content

Commit 5a86d3c

Browse files
committed
feat(mdviewer): copy/cut/paste row and column in table context menus
Add Copy row, Cut row, Paste row, Copy column, Cut column, Paste column items to both the right-click context menu and the row/column handle menus. The clipboard is module-level (lasts the session) and stores cell HTML so styling is preserved on paste. Paste items only appear when their respective clipboard has content. Cut row is disabled for header rows.
1 parent ff253d7 commit 5a86d3c

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

src-mdviewer/src/components/editor.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,67 @@ function deleteTableColumn(table, colIdx) {
735735
});
736736
}
737737

738+
// Module-level clipboard for table row/column copy-paste.
739+
// Stores the inner HTML of each cell so they can be re-inserted into other rows/cols.
740+
let _tableRowClipboard = null; // { cells: [html, ...] }
741+
let _tableColClipboard = null; // { cells: [html, ...] } one per row in source table
742+
743+
function copyTableRow(tr) {
744+
_tableRowClipboard = {
745+
cells: Array.from(tr.children).map(td => td.innerHTML)
746+
};
747+
}
748+
749+
function copyTableColumn(table, colIdx) {
750+
const rows = table.querySelectorAll("tr");
751+
_tableColClipboard = {
752+
cells: Array.from(rows).map(row => {
753+
const cell = row.children[colIdx];
754+
return cell ? cell.innerHTML : "";
755+
})
756+
};
757+
}
758+
759+
function pasteTableRow(table, afterRow) {
760+
if (!_tableRowClipboard) return;
761+
const tbody = table.querySelector("tbody") || table;
762+
const refRow = afterRow || tbody.lastElementChild;
763+
const colCount = refRow ? refRow.children.length : _tableRowClipboard.cells.length;
764+
const newRow = document.createElement("tr");
765+
for (let i = 0; i < colCount; i++) {
766+
const td = document.createElement("td");
767+
td.innerHTML = _tableRowClipboard.cells[i] != null ? _tableRowClipboard.cells[i] : "&nbsp;";
768+
newRow.appendChild(td);
769+
}
770+
if (afterRow && afterRow.nextSibling) {
771+
afterRow.parentNode.insertBefore(newRow, afterRow.nextSibling);
772+
} else if (afterRow) {
773+
afterRow.parentNode.appendChild(newRow);
774+
} else {
775+
tbody.appendChild(newRow);
776+
}
777+
focusCell(newRow.firstElementChild);
778+
return newRow;
779+
}
780+
781+
function pasteTableColumn(table, afterColIdx) {
782+
if (!_tableColClipboard) return;
783+
const rows = table.querySelectorAll("tr");
784+
const insertIdx = afterColIdx != null ? afterColIdx + 1 : (rows[0]?.children.length || 0);
785+
rows.forEach((row, rowIdx) => {
786+
const isHeader = row.parentElement.tagName === "THEAD";
787+
const cell = document.createElement(isHeader ? "th" : "td");
788+
const html = _tableColClipboard.cells[rowIdx];
789+
cell.innerHTML = (html != null && html !== "") ? html : (isHeader ? t("table.header") : "&nbsp;");
790+
const refCell = row.children[insertIdx];
791+
if (refCell) {
792+
row.insertBefore(cell, refCell);
793+
} else {
794+
row.appendChild(cell);
795+
}
796+
});
797+
}
798+
738799
function deleteTable(table) {
739800
const wrapper = table.closest(".table-wrapper");
740801
const target = wrapper || table;
@@ -858,6 +919,10 @@ function showHandleMenu(anchor, type, ctx, contentEl, wrapper, clickX) {
858919
{ label: t("table.insert_row_above"), action: () => { flushSnapshot(contentEl); addTableRow(ctx.table, null, ctx.tr); dispatchInputEvent(contentEl); } },
859920
{ label: t("table.insert_row_below"), action: () => { flushSnapshot(contentEl); addTableRow(ctx.table, ctx.tr); dispatchInputEvent(contentEl); } },
860921
{ divider: true },
922+
{ label: t("table.copy_row"), action: () => { copyTableRow(ctx.tr); } },
923+
{ label: t("table.cut_row"), disabled: ctx.isHeader, action: () => { flushSnapshot(contentEl); copyTableRow(ctx.tr); deleteTableRow(ctx.table, ctx.tr); dispatchInputEvent(contentEl); } },
924+
...(_tableRowClipboard ? [{ label: t("table.paste_row"), action: () => { flushSnapshot(contentEl); pasteTableRow(ctx.table, ctx.tr); dispatchInputEvent(contentEl); } }] : []),
925+
{ divider: true },
861926
{ label: t("table.delete_row"), destructive: true, disabled: ctx.isHeader, action: () => { flushSnapshot(contentEl); deleteTableRow(ctx.table, ctx.tr); dispatchInputEvent(contentEl); } },
862927
{ divider: true },
863928
{ label: t("table.delete_table"), destructive: true, action: () => { flushSnapshot(contentEl); deleteTable(ctx.table); dispatchInputEvent(contentEl); } }
@@ -867,6 +932,10 @@ function showHandleMenu(anchor, type, ctx, contentEl, wrapper, clickX) {
867932
{ label: t("table.insert_col_left"), action: () => { flushSnapshot(contentEl); addTableColumn(ctx.table, ctx.colIdx - 1); dispatchInputEvent(contentEl); } },
868933
{ label: t("table.insert_col_right"), action: () => { flushSnapshot(contentEl); addTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } },
869934
{ divider: true },
935+
{ label: t("table.copy_col"), action: () => { copyTableColumn(ctx.table, ctx.colIdx); } },
936+
{ label: t("table.cut_col"), action: () => { flushSnapshot(contentEl); copyTableColumn(ctx.table, ctx.colIdx); deleteTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } },
937+
...(_tableColClipboard ? [{ label: t("table.paste_col"), action: () => { flushSnapshot(contentEl); pasteTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } }] : []),
938+
{ divider: true },
870939
{ label: t("table.delete_col"), destructive: true, action: () => { flushSnapshot(contentEl); deleteTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } },
871940
{ divider: true },
872941
{ label: t("table.delete_table"), destructive: true, action: () => { flushSnapshot(contentEl); deleteTable(ctx.table); dispatchInputEvent(contentEl); } }
@@ -1091,6 +1160,14 @@ function showTableContextMenu(x, y, ctx, contentEl) {
10911160
{ label: t("table.add_col_left"), action: () => { flushSnapshot(contentEl); addTableColumn(ctx.table, ctx.colIdx - 1); dispatchInputEvent(contentEl); } },
10921161
{ label: t("table.add_col_right"), action: () => { flushSnapshot(contentEl); addTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } },
10931162
{ divider: true },
1163+
{ label: t("table.copy_row"), action: () => { copyTableRow(ctx.tr); } },
1164+
{ label: t("table.cut_row"), action: () => { flushSnapshot(contentEl); copyTableRow(ctx.tr); deleteTableRow(ctx.table, ctx.tr); dispatchInputEvent(contentEl); } },
1165+
...(_tableRowClipboard ? [{ label: t("table.paste_row"), action: () => { flushSnapshot(contentEl); pasteTableRow(ctx.table, ctx.tr); dispatchInputEvent(contentEl); } }] : []),
1166+
{ divider: true },
1167+
{ label: t("table.copy_col"), action: () => { copyTableColumn(ctx.table, ctx.colIdx); } },
1168+
{ label: t("table.cut_col"), action: () => { flushSnapshot(contentEl); copyTableColumn(ctx.table, ctx.colIdx); deleteTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } },
1169+
...(_tableColClipboard ? [{ label: t("table.paste_col"), action: () => { flushSnapshot(contentEl); pasteTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } }] : []),
1170+
{ divider: true },
10941171
{ label: t("table.delete_row"), destructive: true, action: () => { flushSnapshot(contentEl); deleteTableRow(ctx.table, ctx.tr); dispatchInputEvent(contentEl); } },
10951172
{ label: t("table.delete_col"), destructive: true, action: () => { flushSnapshot(contentEl); deleteTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } },
10961173
{ divider: true },

src-mdviewer/src/locales/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@
161161
"add_row_below": "Add row below",
162162
"add_col_left": "Add column left",
163163
"add_col_right": "Add column right",
164+
"copy_row": "Copy row",
165+
"cut_row": "Cut row",
166+
"paste_row": "Paste row below",
167+
"copy_col": "Copy column",
168+
"cut_col": "Cut column",
169+
"paste_col": "Paste column right",
164170
"delete_table": "Delete table"
165171
},
166172
"dialog": {

0 commit comments

Comments
 (0)