diff --git a/src/composables/useCopy.ts b/src/composables/useCopy.ts index d24e7603c5..b2455fef86 100644 --- a/src/composables/useCopy.ts +++ b/src/composables/useCopy.ts @@ -1,6 +1,12 @@ import { useEventListener } from '@vueuse/core' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers' + +const clipboardHTMLWrapper = [ + '
Text' +] /** * Adds a handler on copy that serializes selected nodes to JSON @@ -9,28 +15,19 @@ export const useCopy = () => { const canvasStore = useCanvasStore() useEventListener(document, 'copy', (e) => { - if (!(e.target instanceof Element)) { - return - } - if ( - (e.target instanceof HTMLTextAreaElement && - e.target.type === 'textarea') || - (e.target instanceof HTMLInputElement && e.target.type === 'text') - ) { + if (shouldIgnoreCopyPaste(e.target)) { // Default system copy return } - const isTargetInGraph = - e.target.classList.contains('litegraph') || - e.target.classList.contains('graph-canvas-container') || - e.target.id === 'graph-canvas' - // copy nodes and clear clipboard const canvas = canvasStore.canvas - if (isTargetInGraph && canvas?.selectedItems) { - canvas.copyToClipboard() + if (canvas?.selectedItems) { + const serializedData = canvas.copyToClipboard() // clearData doesn't remove images from clipboard - e.clipboardData?.setData('text', ' ') + e.clipboardData?.setData( + 'text/html', + clipboardHTMLWrapper.join(btoa(serializedData)) + ) e.preventDefault() e.stopImmediatePropagation() return false diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts index c04901951a..551065f0d5 100644 --- a/src/composables/usePaste.ts +++ b/src/composables/usePaste.ts @@ -7,6 +7,22 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { app } from '@/scripts/app' import { useWorkspaceStore } from '@/stores/workspaceStore' import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil' +import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers' + +function pasteClipboardItems(data: DataTransfer): boolean { + const rawData = data.getData('text/html') + const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1] + if (!match) return false + try { + useCanvasStore() + .getCanvas() + ._deserializeItems(JSON.parse(atob(match)), {}) + return true + } catch (err) { + console.error(err) + } + return false +} /** * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data @@ -38,15 +54,10 @@ export const usePaste = () => { } useEventListener(document, 'paste', async (e) => { - const isTargetInGraph = - e.target instanceof Element && - (e.target.classList.contains('litegraph') || - e.target.classList.contains('graph-canvas-container') || - e.target.id === 'graph-canvas') - - // If the target is not in the graph, we don't want to handle the paste event - if (!isTargetInGraph) return - + if (shouldIgnoreCopyPaste(e.target)) { + // Default system copy + return + } // ctrl+shift+v is used to paste nodes with connections // this is handled by litegraph if (workspaceStore.shiftDown) return @@ -109,6 +120,7 @@ export const usePaste = () => { return } } + if (pasteClipboardItems(data)) return // No image found. Look for node data data = data.getData('text/plain') diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index c8424ee6bf..6dbedb475b 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -3853,11 +3853,10 @@ export class LGraphCanvas * When called without parameters, it copies {@link selectedItems}. * @param items The items to copy. If nullish, all selected items are copied. */ - copyToClipboard(items?: Iterable): void { - localStorage.setItem( - 'litegrapheditor_clipboard', - JSON.stringify(this._serializeItems(items)) - ) + copyToClipboard(items?: Iterable): string { + const serializedData = JSON.stringify(this._serializeItems(items)) + localStorage.setItem('litegrapheditor_clipboard', serializedData) + return serializedData } emitEvent(detail: LGraphCanvasEventMap['litegraph:canvas']): void { @@ -3893,6 +3892,7 @@ export class LGraphCanvas if (!data) return return this._deserializeItems(JSON.parse(data), options) } + _deserializeItems( parsed: ClipboardItems, options: IPasteFromClipboardOptions @@ -3909,6 +3909,7 @@ export class LGraphCanvas const { graph } = this if (!graph) throw new NullGraphError() graph.beforeChange() + this.emitBeforeChange() // Parse & initialise parsed.nodes ??= [] @@ -4078,6 +4079,7 @@ export class LGraphCanvas this.selectItems(created) graph.afterChange() + this.emitAfterChange() return results } diff --git a/src/workbench/eventHelpers.ts b/src/workbench/eventHelpers.ts new file mode 100644 index 0000000000..1030f3415d --- /dev/null +++ b/src/workbench/eventHelpers.ts @@ -0,0 +1,30 @@ +/** + * Utility functions for handling workbench events + */ + +/** + * Used by clipboard handlers to determine if copy/paste events should be + * intercepted for graph operations vs. allowing default browser behavior + * for text inputs and other UI elements. + * + * @param target - The event target to check + * @returns true if copy paste events will be handled by target + */ +export function shouldIgnoreCopyPaste(target: EventTarget | null): boolean { + return ( + target instanceof HTMLTextAreaElement || + (target instanceof HTMLInputElement && + ![ + 'button', + 'checkbox', + 'file', + 'hidden', + 'image', + 'radio', + 'range', + 'reset', + 'search', + 'submit' + ].includes(target.type)) + ) +}