Collapse encoded media in editable fields too (#534)

This commit is contained in:
Gregory Schier
2026-08-13 12:56:32 -07:00
committed by GitHub
parent c596e25e5e
commit 7e2db7799b
8 changed files with 343 additions and 82 deletions
+16
View File
@@ -16,6 +16,8 @@ export interface ContextMenuInstance {
triggerPosition: { x: number; y: number };
/** The trigger's box, when it has one, so placement can align to its edges */
triggerRect?: Pick<DOMRect, "top" | "bottom" | "left" | "right">;
/** The element it belongs to, so clicking that doesn't read as a click outside */
triggerEl?: HTMLElement | null;
items: DropdownItem[];
}
@@ -25,6 +27,20 @@ export function showContextMenu({ id, ...props }: ContextMenuInstance) {
jotaiStore.set(contextMenusAtom, (m) => [...m.filter((c) => c.id !== id), { id, ...props }]);
}
/**
* Opens the menu, or closes it if this one is already open.
*
* What a trigger wants: pressing it a second time should put the menu away. Same shape as
* {@link toggleDialog}.
*/
export function toggleContextMenu({ id, ...props }: ContextMenuInstance) {
if (jotaiStore.get(contextMenusAtom).some((c) => c.id === id)) {
hideContextMenu(id);
} else {
showContextMenu({ id, ...props });
}
}
export function hideContextMenu(id: string) {
jotaiStore.set(contextMenusAtom, (m) => m.filter((c) => c.id !== id));
}
+21 -8
View File
@@ -60,10 +60,23 @@ export function largeValueActions({
});
}
// Two ways to copy a picture, because either can be the one you wanted: the image to paste
// somewhere that takes one, or the text to paste back into a request
const image = isCopyableImage(sniffed);
if (image) {
items.push({
label: "Copy Image",
leftSlot: createElement(Icon, { icon: "copy" }),
onSelect: () => copyImage(value(), sniffed, copyText()),
});
}
items.push({
label: sniffed?.mime.startsWith("image/") ? "Copy Image" : "Copy",
leftSlot: createElement(Icon, { icon: "copy" }),
onSelect: () => copyValue(value(), sniffed, copyText()),
label: sniffed?.encoding === "base64" ? "Copy Base64" : "Copy",
// Second of a pair reads as one thing with two options, so it keeps the indent without
// repeating the icon above it
leftSlot: createElement(Icon, { icon: image ? "empty" : "copy" }),
onSelect: () => copyToClipboard(copyText()),
});
items.push({
@@ -167,15 +180,15 @@ async function toPngBlob(text: string, sniffed: SniffedValue): Promise<Blob> {
}
/**
* Copies the value: the picture itself when it is one, and the text it stands for otherwise.
* Puts the picture on the clipboard, so it can be pasted anywhere that takes an image.
*
* Not awaited, and deliberately so. The webview only allows a clipboard write that starts inside
* the click that asked for it, and decoding a few megabytes takes longer than that lasts — so
* the item is handed the still-pending promise rather than a finished blob.
*/
export function copyValue(text: string, sniffed: SniffedValue | null, hidden: string) {
if (!isCopyableImage(sniffed) || typeof ClipboardItem === "undefined") {
copyToClipboard(hidden);
export function copyImage(text: string, sniffed: SniffedValue, fallback: string) {
if (typeof ClipboardItem === "undefined") {
copyToClipboard(fallback);
return;
}
@@ -195,7 +208,7 @@ export function copyValue(text: string, sniffed: SniffedValue | null, hidden: st
// Anything from a webview that won't take an image to a file we couldn't decode. The
// encoded text is always there to fall back on.
console.error("Failed to copy image, copying text instead", err);
copyToClipboard(hidden);
copyToClipboard(fallback);
});
}