Skip to content

Commit b91af25

Browse files
committed
feat(mdviewer): add clipboard permissions and table context menu paste
Add allow-clipboard-read/write sandbox flags and permissions policy on the md viewer iframe for Chromium context menu paste support. Hide paste menu items on browsers where Clipboard API is blocked (Safari/Firefox sandboxed iframes). Add cut/copy/paste items to the table right-click context menu.
1 parent b0db69d commit b91af25

3 files changed

Lines changed: 74 additions & 14 deletions

File tree

src-mdviewer/src/components/context-menu.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@ let menu = null;
88
let cleanupFns = [];
99
let savedRange = null;
1010

11+
// Detect clipboard API availability (blocked in Safari/Firefox sandboxed iframes)
12+
let _clipboardApiSupported = null;
13+
function _isClipboardApiAvailable() {
14+
if (_clipboardApiSupported !== null) { return _clipboardApiSupported; }
15+
if (!navigator.clipboard || !navigator.clipboard.readText) {
16+
_clipboardApiSupported = false;
17+
return false;
18+
}
19+
// Probe by calling readText — if permissions policy blocks it, it throws synchronously
20+
// or rejects immediately. Cache the result after first context menu open.
21+
navigator.clipboard.readText()
22+
.then(() => { _clipboardApiSupported = true; })
23+
.catch(() => { _clipboardApiSupported = false; });
24+
// Optimistic for first open on Chromium; will correct on next open if blocked
25+
_clipboardApiSupported = true;
26+
return _clipboardApiSupported;
27+
}
28+
1129
export function initContextMenu() {
1230
menu = document.getElementById("context-menu");
1331
if (!menu) return;
@@ -141,16 +159,20 @@ function buildItems(ctx) {
141159
});
142160
}
143161

144-
items.push({
145-
label: t("context.paste"),
146-
shortcut: `${modLabel}+V`,
147-
action: () => pasteFromClipboard(false)
148-
});
149-
items.push({
150-
label: t("context.paste_plain"),
151-
shortcut: `${modLabel}+\u21E7+V`,
152-
action: () => pasteFromClipboard(true)
153-
});
162+
// Clipboard API paste only works in Chromium with permissions policy.
163+
// In Safari/Firefox sandboxed iframes, it's blocked. Users can still Ctrl/Cmd+V.
164+
if (_isClipboardApiAvailable()) {
165+
items.push({
166+
label: t("context.paste"),
167+
shortcut: `${modLabel}+V`,
168+
action: () => pasteFromClipboard(false)
169+
});
170+
items.push({
171+
label: t("context.paste_plain"),
172+
shortcut: `${modLabel}+\u21E7+V`,
173+
action: () => pasteFromClipboard(true)
174+
});
175+
}
154176

155177
items.push({ divider: true });
156178

src-mdviewer/src/components/editor.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,44 @@ function showTableContextMenu(x, y, ctx, contentEl) {
10241024
const menu = document.getElementById("table-context-menu");
10251025
if (!menu) return;
10261026

1027-
const items = [
1027+
const sel = window.getSelection();
1028+
const hasSelection = sel && !sel.isCollapsed;
1029+
1030+
const items = [];
1031+
1032+
// Cut/Copy/Paste
1033+
if (hasSelection) {
1034+
items.push({
1035+
label: t("context.cut") || "Cut",
1036+
action: () => { document.execCommand("cut"); }
1037+
});
1038+
items.push({
1039+
label: t("context.copy") || "Copy",
1040+
action: () => { document.execCommand("copy"); }
1041+
});
1042+
}
1043+
if (navigator.clipboard && navigator.clipboard.readText) {
1044+
items.push({
1045+
label: t("context.paste") || "Paste",
1046+
action: async () => {
1047+
contentEl.focus({ preventScroll: true });
1048+
try {
1049+
const text = await navigator.clipboard.readText();
1050+
if (text) {
1051+
document.execCommand("insertText", false, text.replace(/[\r\n]+/g, " ").trim());
1052+
}
1053+
} catch {
1054+
document.execCommand("paste");
1055+
}
1056+
}
1057+
});
1058+
}
1059+
if (hasSelection || (navigator.clipboard && navigator.clipboard.readText)) {
1060+
items.push({ divider: true });
1061+
}
1062+
1063+
// Table operations
1064+
items.push(
10281065
{ label: t("table.add_row_above"), action: () => { flushSnapshot(contentEl); addTableRow(ctx.table, null, ctx.tr); dispatchInputEvent(contentEl); } },
10291066
{ label: t("table.add_row_below"), action: () => { flushSnapshot(contentEl); addTableRow(ctx.table, ctx.tr); dispatchInputEvent(contentEl); } },
10301067
{ label: t("table.add_col_left"), action: () => { flushSnapshot(contentEl); addTableColumn(ctx.table, ctx.colIdx - 1); dispatchInputEvent(contentEl); } },
@@ -1034,7 +1071,7 @@ function showTableContextMenu(x, y, ctx, contentEl) {
10341071
{ label: t("table.delete_col"), destructive: true, action: () => { flushSnapshot(contentEl); deleteTableColumn(ctx.table, ctx.colIdx); dispatchInputEvent(contentEl); } },
10351072
{ divider: true },
10361073
{ label: t("table.delete_table"), destructive: true, action: () => { flushSnapshot(contentEl); deleteTable(ctx.table); dispatchInputEvent(contentEl); } }
1037-
];
1074+
);
10381075

10391076
menu.innerHTML = "";
10401077
items.forEach((item) => {

src/extensionsIntegrated/Phoenix-live-preview/main.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,13 @@ define(function (require, exports, module) {
141141
// no allow-forms, allow-pointer-lock (not needed for markdown editing).
142142
// Communication works via MarkdownSync's own message handler (bypasses EventManager origin check).
143143
const _mdSandboxAttr = Phoenix.isTestWindow ? "" :
144-
'sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-modals"';
144+
'sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-modals allow-clipboard-read allow-clipboard-write"';
145145
const MDVIEWR_IFRAME_HTML = `
146146
<iframe id="${MDVIEWR_IFRAME_ID}" title="Markdown Preview" style="border: none"
147147
width="100%" height="100%" seamless="true"
148148
src='about:blank'
149-
${_mdSandboxAttr}>
149+
${_mdSandboxAttr}
150+
allow="clipboard-read; clipboard-write">
150151
</iframe>
151152
`;
152153

0 commit comments

Comments
 (0)