fix(chat): preserve native copying of selected text (#540)
- Allow native text selection in AI chat messages.\n- Leave active DOM text ranges to the platform clipboard.\n- Preserve canvas clipboard routing when no document text is selected.
This commit is contained in:
parent
54c1373d3e
commit
d112bc9044
|
|
@ -55,6 +55,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Let desktop users select and copy AI chat text without replacing it with the selected canvas layers. (#538)
|
||||
- Restore visible above, below, and child drop feedback while dragging layers in the Layers panel.
|
||||
- Place editor-created instances beside nested source components in world space, including transformed source and destination parents.
|
||||
- Harden collaboration node synchronization against malformed remote source metadata and geometry while excluding derived text-renderer caches.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
copySelectionToTauriClipboard,
|
||||
pasteFromTauriClipboard
|
||||
} from '@/app/editor/clipboard/system'
|
||||
import { isEditing } from '@/app/shell/keyboard/focus'
|
||||
import { hasDocumentTextSelection, isEditing } from '@/app/shell/keyboard/focus'
|
||||
import { isTauri } from '@/app/tauri/env'
|
||||
|
||||
function cursorPosition(store: EditorStore) {
|
||||
|
|
@ -17,7 +17,7 @@ function cursorPosition(store: EditorStore) {
|
|||
|
||||
export function bindEditorClipboard(store: EditorStore) {
|
||||
useEventListener(window, 'copy', (e: ClipboardEvent) => {
|
||||
if (isEditing(e)) return
|
||||
if (isEditing(e) || hasDocumentTextSelection()) return
|
||||
e.preventDefault()
|
||||
if (isTauri()) {
|
||||
void copySelectionToTauriClipboard(store)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ export function isEditing(event: Event) {
|
|||
return event.composedPath().some(isEditableTarget)
|
||||
}
|
||||
|
||||
export function hasDocumentTextSelection(): boolean {
|
||||
const selection = window.getSelection()
|
||||
return selection !== null && !selection.isCollapsed && selection.toString().length > 0
|
||||
}
|
||||
|
||||
export function isInputElement(element: EventTarget | null | undefined): boolean {
|
||||
return (
|
||||
element instanceof HTMLInputElement ||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ function partKey(part: UIMessagePart<UIDataTypes, UITools>, index: number): stri
|
|||
v-test-id="`chat-message-${message.role}`"
|
||||
:class="message.role === 'user' ? 'flex justify-end' : ''"
|
||||
>
|
||||
<div class="min-w-0 space-y-2" :class="message.role === 'user' ? 'max-w-[85%]' : ''">
|
||||
<div
|
||||
class="min-w-0 space-y-2 select-text"
|
||||
:class="message.role === 'user' ? 'max-w-[85%]' : ''"
|
||||
>
|
||||
<template v-if="message.role === 'assistant'">
|
||||
<template v-for="(part, i) in message.parts" :key="partKey(part, i)">
|
||||
<!-- Tool call -->
|
||||
|
|
|
|||
|
|
@ -36,6 +36,18 @@ function selectedCount(page: Page): Promise<number> {
|
|||
})
|
||||
}
|
||||
|
||||
async function selectProbeText(page: Page, selector: string) {
|
||||
await page.evaluate((probeSelector) => {
|
||||
const probe = document.querySelector(probeSelector)
|
||||
const selection = window.getSelection()
|
||||
if (!probe || !selection) throw new Error(`Text selection probe not found: ${probeSelector}`)
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(probe)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}, selector)
|
||||
}
|
||||
|
||||
async function dispatchClipboardEvent(
|
||||
page: Page,
|
||||
type: 'copy' | 'cut' | 'paste',
|
||||
|
|
@ -147,6 +159,33 @@ test('Tauri clipboard events from editable fields keep native text behavior', as
|
|||
expect(await pageChildren(page)).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('Tauri copy preserves selected document text instead of copying canvas layers', async ({
|
||||
page
|
||||
}) => {
|
||||
const { canvas, clipboard } = await createTauriEditorPage(page)
|
||||
await canvas.drawRect(160, 160, 96, 72)
|
||||
const before = clipboard.snapshot()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const probe = document.createElement('div')
|
||||
probe.className = 'select-text'
|
||||
probe.dataset.clipboardProbe = 'document-text'
|
||||
probe.textContent = 'Assistant response'
|
||||
document.body.append(probe)
|
||||
})
|
||||
await selectProbeText(page, '[data-clipboard-probe="document-text"]')
|
||||
expect(
|
||||
await page.evaluate(() => ({
|
||||
collapsed: window.getSelection()?.isCollapsed,
|
||||
text: window.getSelection()?.toString()
|
||||
}))
|
||||
).toEqual({ collapsed: false, text: 'Assistant response' })
|
||||
await dispatchClipboardEvent(page, 'copy', '[data-clipboard-probe="document-text"]')
|
||||
|
||||
expect(clipboard.snapshot()).toEqual(before)
|
||||
expect(await selectedCount(page)).toBe(1)
|
||||
})
|
||||
|
||||
test('Tauri context-menu copy uses plugin clipboard fallback instead of blocked browser command', async ({
|
||||
page
|
||||
}) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue