feat(markdown): add paste detection with PasteChoicePopup for text/markdown/image
Simplify Ctrl+V shortcut to only handle recent internal copies, letting native paste events flow to setupPaste for external clipboard content. Extend setupPaste with text/HTML detection and popup callbacks. Wire PasteChoicePopup in Editor.tsx with markdown (turndown HTML-to-MD), plain text, and image paste choices.
This commit is contained in:
@@ -324,11 +324,38 @@ export function setupPaste(
|
||||
onChange: OnChange,
|
||||
selection?: SelectionManager | null,
|
||||
uploads?: UploadManager | null,
|
||||
opts?: {
|
||||
onTextPaste?: (data: { text: string; html: string; hasImage: boolean }) => void;
|
||||
onShortTextPaste?: (text: string) => void;
|
||||
},
|
||||
): () => void {
|
||||
async function onPaste(e: ClipboardEvent) {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
|
||||
// ── Text/HTML detection — before media loop ──
|
||||
let hasMedia = false;
|
||||
let textContent = '';
|
||||
let htmlContent = '';
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.type.startsWith('image/') || item.type.startsWith('video/')) hasMedia = true;
|
||||
if (item.type === 'text/plain') textContent = e.clipboardData?.getData('text/plain') || '';
|
||||
if (item.type === 'text/html') htmlContent = e.clipboardData?.getData('text/html') || '';
|
||||
}
|
||||
|
||||
if (textContent) {
|
||||
e.preventDefault();
|
||||
if (textContent.length > 20) {
|
||||
opts?.onTextPaste?.({ text: textContent, html: htmlContent, hasImage: hasMedia });
|
||||
} else {
|
||||
opts?.onShortTextPaste?.(textContent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Media paste logic (unchanged) ──
|
||||
const newItemIds: string[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
@@ -493,36 +493,19 @@ export const shortcuts: ShortcutDef[] = [
|
||||
id: 'paste', keys: { key: 'v', ctrl: true },
|
||||
category: 'editing', description: 'Paste',
|
||||
handler: async (ctx) => {
|
||||
// Strategy: Check system clipboard for images first.
|
||||
// - If system clipboard has an image AND we did NOT just do an internal copy
|
||||
// (or it's been a while), paste from system clipboard (external image).
|
||||
// - If we just did an internal copy (lastInternalCopyTime is recent),
|
||||
// use internal clipboard to duplicate scene items (preserves vector data).
|
||||
// - If system clipboard has no images, fall back to internal clipboard.
|
||||
|
||||
// Only handle recent internal copies here.
|
||||
// For external clipboard content (images, text, HTML), do NOT call
|
||||
// e.preventDefault() — let the native paste event fire through to setupPaste
|
||||
// in image-drop.ts, which is the single path for all external clipboard content.
|
||||
const timeSinceInternalCopy = Date.now() - _lastInternalCopyTime;
|
||||
const hasInternalItems = ctx.clipboardRef.current.length > 0;
|
||||
const recentInternalCopy = hasInternalItems && timeSinceInternalCopy < 500;
|
||||
|
||||
// If we JUST did an internal copy (<500ms ago), use internal clipboard
|
||||
// (the system clipboard image is just the rasterized version of what we copied)
|
||||
if (recentInternalCopy) {
|
||||
await _pasteInternal(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try system clipboard first
|
||||
try {
|
||||
const result = await ctx.pasteFromSystemClipboard();
|
||||
if (result === 'Pasted image') return;
|
||||
} catch {
|
||||
// Clipboard API denied or unavailable — fall through
|
||||
}
|
||||
|
||||
// Fall back to internal clipboard
|
||||
if (hasInternalItems) {
|
||||
await _pasteInternal(ctx);
|
||||
}
|
||||
// For external clipboard content: do nothing, let native paste event fire through to setupPaste
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user