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))
+ )
+}