fix(desktop): respect native clipboard regions

This commit is contained in:
t8y2 committed 2026-06-23 16:12:35 +08:00
1 parent 8ae9863c51
commit c32a95c477
4 files changed
+106 -24

No files matched your search

@@ -182,10 +182,10 @@ onBeforeUnmount(() => {
</div>
<!-- Shiki highlighted SQL -->
<div v-else-if="highlightedHtml" class="p-3 text-xs leading-relaxed [&_pre]:!bg-transparent [&_pre]:!p-0 [&_code]:!font-mono [&_code]:text-xs" v-html="highlightedHtml" />
<div v-else-if="highlightedHtml" data-native-clipboard class="p-3 text-xs leading-relaxed [&_pre]:!bg-transparent [&_pre]:!p-0 [&_code]:!font-mono [&_code]:text-xs" v-html="highlightedHtml" />
<!-- Plain text fallback -->
<pre v-else class="p-3 text-xs font-mono whitespace-pre-wrap select-text">{{ displaySql }}</pre>
<pre v-else data-native-clipboard class="p-3 text-xs font-mono whitespace-pre-wrap select-text">{{ displaySql }}</pre>
</div>
</div>
</template>
+2 -21
View File
@@ -129,7 +129,7 @@ import { parseClipboardTable } from "@/lib/gridSelection";
import { useToast } from "@/composables/useToast";
import { useDataGridExport } from "@/composables/useDataGridExport";
import { readTextFromClipboard } from "@/lib/clipboard";
import { eventTargetAllowsNativeClipboard, isPlainClipboardShortcut, readTextFromClipboard } from "@/lib/clipboard";
import ExportProgressDialog from "@/components/export/ExportProgressDialog.vue";
import { DATA_GRID_ROW_NUM_WIDTH, useDataGridColumnResize } from "@/composables/useDataGridColumnResize";
import { useDataGridSelection } from "@/composables/useDataGridSelection";
@@ -4746,25 +4746,6 @@ function openImagePreview(src: string, title: string) {
imagePreviewOpen.value = true;
}
function selectionNodeElement(node: Node | null): Element | null {
if (!node) return null;
return node instanceof Element ? node : node.parentElement;
}
function hasNativeClipboardSelection(): boolean {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) return false;
const anchorRegion = selectionNodeElement(selection.anchorNode)?.closest("[data-native-clipboard]");
const focusRegion = selectionNodeElement(selection.focusNode)?.closest("[data-native-clipboard]");
return !!anchorRegion && anchorRegion === focusRegion;
}
function eventTargetAllowsNativeClipboard(event: KeyboardEvent): boolean {
const target = event.target as HTMLElement | null;
if (target?.closest("input, textarea, [contenteditable='true'], [role='textbox']")) return true;
return clipboardShortcut(event, "c") && hasNativeClipboardSelection();
}
function onDrawerContextMenu(event: MouseEvent) {
event.stopPropagation();
const target = event.target as HTMLElement | null;
@@ -4773,7 +4754,7 @@ function onDrawerContextMenu(event: MouseEvent) {
}
function clipboardShortcut(event: KeyboardEvent, key: string): boolean {
return (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === key;
return isPlainClipboardShortcut(event, key);
}
let lastPasteEventAt = 0;
+49
View File
@@ -37,6 +37,55 @@ export interface ClipboardEnvironment {
document?: ClipboardDocument;
}
export interface ClipboardShortcutEvent {
key: string;
metaKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
target?: EventTarget | null;
}
interface SelectionLike {
anchorNode: Node | null;
focusNode: Node | null;
isCollapsed: boolean;
}
interface NativeClipboardSelectionEnvironment {
getSelection?: () => SelectionLike | null;
}
const EDITABLE_CLIPBOARD_TARGET_SELECTOR = "input, textarea, [contenteditable='true'], [role='textbox']";
const NATIVE_CLIPBOARD_REGION_SELECTOR = "[data-native-clipboard]";
function closestElement(target: unknown, selector: string): unknown {
return (target as { closest?: (selector: string) => unknown } | null)?.closest?.(selector) ?? null;
}
function selectionNodeElement(node: Node | null): Element | null {
if (!node) return null;
if (typeof Element !== "undefined" && node instanceof Element) return node;
return (node as { parentElement?: Element | null }).parentElement ?? null;
}
export function isPlainClipboardShortcut(event: ClipboardShortcutEvent, key: string): boolean {
return !!(event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === key;
}
export function hasNativeClipboardSelection(env: NativeClipboardSelectionEnvironment = globalThis as unknown as NativeClipboardSelectionEnvironment): boolean {
const selection = env.getSelection?.();
if (!selection || selection.isCollapsed) return false;
const anchorRegion = selectionNodeElement(selection.anchorNode)?.closest(NATIVE_CLIPBOARD_REGION_SELECTOR);
const focusRegion = selectionNodeElement(selection.focusNode)?.closest(NATIVE_CLIPBOARD_REGION_SELECTOR);
return !!anchorRegion && anchorRegion === focusRegion;
}
export function eventTargetAllowsNativeClipboard(event: ClipboardShortcutEvent, env: NativeClipboardSelectionEnvironment = globalThis as unknown as NativeClipboardSelectionEnvironment): boolean {
if (closestElement(event.target, EDITABLE_CLIPBOARD_TARGET_SELECTOR)) return true;
return isPlainClipboardShortcut(event, "c") && hasNativeClipboardSelection(env);
}
export async function readTextFromClipboard(env: ClipboardEnvironment = globalThis as unknown as ClipboardEnvironment): Promise<string> {
if (isTauriRuntime(env as unknown as Record<string, unknown>)) {
try {