Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions src/composables/useCopy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { useEventListener } from '@vueuse/core'

import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'

const clipboardHTMLWrapper = [
'<meta charset="utf-8"><div><span data-metadata="',
'"></span></div><span style="white-space:pre-wrap;">Text</span>'
]

/**
* Adds a handler on copy that serializes selected nodes to JSON
Expand All @@ -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
Expand Down
30 changes: 21 additions & 9 deletions src/composables/usePaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[architecture] high Priority

Issue: Cross-cutting concerns violation - direct access to canvas store within utility function
Context: The pasteClipboardItems function directly calls useCanvasStore() which breaks the dependency injection pattern used in Vue composables
Suggestion: Pass canvas instance as parameter: pasteClipboardItems(data: DataTransfer, canvas: LGraphCanvas): boolean

.getCanvas()
._deserializeItems(JSON.parse(atob(match)), {})
return true
} catch (err) {
console.error(err)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[quality] high Priority

Issue: Generic error handling that swallows specific error details
Context: Using console.error(err) and returning false makes debugging difficult for users when clipboard operations fail
Suggestion: Log specific error message with context like 'Failed to parse clipboard data:' + err.message or provide user-friendly feedback

}
return false
}

/**
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -109,6 +120,7 @@ export const usePaste = () => {
return
}
}
if (pasteClipboardItems(data)) return

// No image found. Look for node data
data = data.getData('text/plain')
Expand Down
12 changes: 7 additions & 5 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Positionable>): void {
localStorage.setItem(
'litegrapheditor_clipboard',
JSON.stringify(this._serializeItems(items))
)
copyToClipboard(items?: Iterable<Positionable>): string {
const serializedData = JSON.stringify(this._serializeItems(items))
localStorage.setItem('litegrapheditor_clipboard', serializedData)
return serializedData
}

emitEvent(detail: LGraphCanvasEventMap['litegraph:canvas']): void {
Expand Down Expand Up @@ -3893,6 +3892,7 @@ export class LGraphCanvas
if (!data) return
return this._deserializeItems(JSON.parse(data), options)
}

_deserializeItems(
parsed: ClipboardItems,
options: IPasteFromClipboardOptions
Expand All @@ -3909,6 +3909,7 @@ export class LGraphCanvas
const { graph } = this
if (!graph) throw new NullGraphError()
graph.beforeChange()
this.emitBeforeChange()

// Parse & initialise
parsed.nodes ??= []
Expand Down Expand Up @@ -4078,6 +4079,7 @@ export class LGraphCanvas
this.selectItems(created)

graph.afterChange()
this.emitAfterChange()

return results
}
Expand Down
30 changes: 30 additions & 0 deletions src/workbench/eventHelpers.ts
Original file line number Diff line number Diff line change
@@ -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))
)
}