Files
dbx/apps/desktop/src/lib/clipboard.ts
T

151 lines
4.8 KiB
TypeScript

import { isTauriRuntime } from "./tauriRuntime";
interface ClipboardApi {
readText?: () => Promise<string> | string;
writeText?: (text: string) => Promise<void> | void;
}
interface ClipboardNavigator {
clipboard?: ClipboardApi;
}
interface ClipboardTextarea {
value: string;
style: {
position?: string;
top?: string;
left?: string;
opacity?: string;
};
setAttribute(name: string, value: string): void;
focus?(): void;
select(): void;
setSelectionRange?(start: number, end: number): void;
}
interface ClipboardDocument {
body?: {
appendChild(node: unknown): unknown;
removeChild(node: unknown): unknown;
};
createElement(tagName: "textarea"): ClipboardTextarea;
execCommand?(command: string): boolean;
}
export interface ClipboardEnvironment {
navigator?: ClipboardNavigator;
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 {
const { readText } = await import("@tauri-apps/plugin-clipboard-manager");
return await readText();
} catch {
// Fall through to Web Clipboard when the native plugin is unavailable.
}
}
if (env.navigator?.clipboard?.readText) {
return await env.navigator.clipboard.readText();
}
throw new Error("Clipboard API is not available");
}
export async function copyToClipboard(text: string, env: ClipboardEnvironment = globalThis as unknown as ClipboardEnvironment): Promise<void> {
if (isTauriRuntime(env as unknown as Record<string, unknown>)) {
try {
const { writeText } = await import("@tauri-apps/plugin-clipboard-manager");
await writeText(text);
return;
} catch {
// Fall through to Web Clipboard / legacy copy when the native plugin is unavailable.
}
}
try {
if (env.navigator?.clipboard?.writeText) {
await env.navigator.clipboard.writeText(text);
return;
}
} catch {
// Fall through to the legacy copy path for non-secure web contexts.
}
const document = env.document;
if (!document?.body || !document.execCommand) {
throw new Error("Clipboard API is not available");
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.top = "0";
textarea.style.left = "-9999px";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
try {
textarea.focus?.();
textarea.select();
textarea.setSelectionRange?.(0, text.length);
if (!document.execCommand("copy")) {
throw new Error("Clipboard copy failed");
}
} finally {
document.body.removeChild(textarea);
}
}