mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 01:50:08 +00:00
[backport core/1.33] fix: handle Unicode characters in clipboard copy/paste and add Paste menu option (#7246)
Backport of #7103 to `core/1.33` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7246-backport-core-1-33-fix-handle-Unicode-characters-in-clipboard-copy-paste-and-add-Paste-2c36d73d365081d79d8de04456bdf9d8) by [Unito](https://www.unito.io) Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
@@ -23,10 +23,16 @@ export const useCopy = () => {
|
||||
const canvas = canvasStore.canvas
|
||||
if (canvas?.selectedItems) {
|
||||
const serializedData = canvas.copyToClipboard()
|
||||
// Use TextEncoder to handle Unicode characters properly
|
||||
const base64Data = btoa(
|
||||
String.fromCharCode(
|
||||
...Array.from(new TextEncoder().encode(serializedData))
|
||||
)
|
||||
)
|
||||
// clearData doesn't remove images from clipboard
|
||||
e.clipboardData?.setData(
|
||||
'text/html',
|
||||
clipboardHTMLWrapper.join(btoa(serializedData))
|
||||
clipboardHTMLWrapper.join(base64Data)
|
||||
)
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
|
||||
@@ -14,9 +14,11 @@ function pasteClipboardItems(data: DataTransfer): boolean {
|
||||
const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1]
|
||||
if (!match) return false
|
||||
try {
|
||||
useCanvasStore()
|
||||
.getCanvas()
|
||||
._deserializeItems(JSON.parse(atob(match)), {})
|
||||
// Decode UTF-8 safe base64
|
||||
const binaryString = atob(match)
|
||||
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0))
|
||||
const decodedData = new TextDecoder().decode(bytes)
|
||||
useCanvasStore().getCanvas()._deserializeItems(JSON.parse(decodedData), {})
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -8031,7 +8031,13 @@ export class LGraphCanvas
|
||||
has_submenu: true,
|
||||
callback: LGraphCanvas.onMenuAdd
|
||||
},
|
||||
{ content: 'Add Group', callback: LGraphCanvas.onGroupAdd }
|
||||
{ content: 'Add Group', callback: LGraphCanvas.onGroupAdd },
|
||||
{
|
||||
content: 'Paste',
|
||||
callback: () => {
|
||||
this.pasteFromClipboard()
|
||||
}
|
||||
}
|
||||
// { content: "Arrange", callback: that.graph.arrange },
|
||||
// {content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll }
|
||||
]
|
||||
|
||||
105
tests-ui/tests/composables/clipboard.test.ts
Normal file
105
tests-ui/tests/composables/clipboard.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* Encodes a UTF-8 string to base64 (same logic as useCopy.ts)
|
||||
*/
|
||||
function encodeClipboardData(data: string): string {
|
||||
return btoa(
|
||||
String.fromCharCode(...Array.from(new TextEncoder().encode(data)))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes base64 to UTF-8 string (same logic as usePaste.ts)
|
||||
*/
|
||||
function decodeClipboardData(base64: string): string {
|
||||
const binaryString = atob(base64)
|
||||
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0))
|
||||
return new TextDecoder().decode(bytes)
|
||||
}
|
||||
|
||||
describe('Clipboard UTF-8 base64 encoding/decoding', () => {
|
||||
it('should handle ASCII-only strings', () => {
|
||||
const original = '{"nodes":[{"id":1,"type":"LoadImage"}]}'
|
||||
const encoded = encodeClipboardData(original)
|
||||
const decoded = decodeClipboardData(encoded)
|
||||
expect(decoded).toBe(original)
|
||||
})
|
||||
|
||||
it('should handle Chinese characters in localized_name', () => {
|
||||
const original =
|
||||
'{"nodes":[{"id":1,"type":"LoadImage","localized_name":"图像"}]}'
|
||||
const encoded = encodeClipboardData(original)
|
||||
const decoded = decodeClipboardData(encoded)
|
||||
expect(decoded).toBe(original)
|
||||
})
|
||||
|
||||
it('should handle Japanese characters', () => {
|
||||
const original = '{"localized_name":"画像を読み込む"}'
|
||||
const encoded = encodeClipboardData(original)
|
||||
const decoded = decodeClipboardData(encoded)
|
||||
expect(decoded).toBe(original)
|
||||
})
|
||||
|
||||
it('should handle Korean characters', () => {
|
||||
const original = '{"localized_name":"이미지 불러오기"}'
|
||||
const encoded = encodeClipboardData(original)
|
||||
const decoded = decodeClipboardData(encoded)
|
||||
expect(decoded).toBe(original)
|
||||
})
|
||||
|
||||
it('should handle mixed ASCII and Unicode characters', () => {
|
||||
const original =
|
||||
'{"nodes":[{"id":1,"type":"LoadImage","localized_name":"加载图像","label":"Load Image 图片"}]}'
|
||||
const encoded = encodeClipboardData(original)
|
||||
const decoded = decodeClipboardData(encoded)
|
||||
expect(decoded).toBe(original)
|
||||
})
|
||||
|
||||
it('should handle emoji characters', () => {
|
||||
const original = '{"title":"Test Node 🎨🖼️"}'
|
||||
const encoded = encodeClipboardData(original)
|
||||
const decoded = decodeClipboardData(encoded)
|
||||
expect(decoded).toBe(original)
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const original = ''
|
||||
const encoded = encodeClipboardData(original)
|
||||
const decoded = decodeClipboardData(encoded)
|
||||
expect(decoded).toBe(original)
|
||||
})
|
||||
|
||||
it('should handle complex node data with multiple Unicode fields', () => {
|
||||
const original = JSON.stringify({
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'LoadImage',
|
||||
localized_name: '图像',
|
||||
inputs: [{ localized_name: '图片', name: 'image' }],
|
||||
outputs: [{ localized_name: '输出', name: 'output' }]
|
||||
}
|
||||
],
|
||||
groups: [{ title: '预处理组 🔧' }],
|
||||
links: []
|
||||
})
|
||||
const encoded = encodeClipboardData(original)
|
||||
const decoded = decodeClipboardData(encoded)
|
||||
expect(decoded).toBe(original)
|
||||
expect(JSON.parse(decoded)).toEqual(JSON.parse(original))
|
||||
})
|
||||
|
||||
it('should produce valid base64 output', () => {
|
||||
const original = '{"localized_name":"中文测试"}'
|
||||
const encoded = encodeClipboardData(original)
|
||||
// Base64 should only contain valid characters
|
||||
expect(encoded).toMatch(/^[A-Za-z0-9+/=]+$/)
|
||||
})
|
||||
|
||||
it('should fail with plain btoa for non-Latin1 characters', () => {
|
||||
const original = '{"localized_name":"图像"}'
|
||||
// This demonstrates why we need TextEncoder - plain btoa fails
|
||||
expect(() => btoa(original)).toThrow()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user