65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
/** Copy only after a user action; WebViews may not expose the async Clipboard API. */
|
|
export async function copyText(text: string): Promise<void> {
|
|
if (navigator.clipboard?.writeText) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return;
|
|
} catch {
|
|
// Embedded browsers can deny the async API while allowing a selected copy.
|
|
}
|
|
}
|
|
|
|
const active = document.activeElement;
|
|
const selection = window.getSelection();
|
|
const ranges = selection
|
|
? Array.from({ length: selection.rangeCount }, (_, index) =>
|
|
selection.getRangeAt(index).cloneRange(),
|
|
)
|
|
: [];
|
|
const inputSelection =
|
|
active instanceof HTMLInputElement ||
|
|
active instanceof HTMLTextAreaElement
|
|
? ([
|
|
active.selectionStart,
|
|
active.selectionEnd,
|
|
active.selectionDirection,
|
|
] as const)
|
|
: null;
|
|
const field = document.createElement('textarea');
|
|
field.value = text;
|
|
field.readOnly = true;
|
|
field.style.cssText =
|
|
'position:fixed;opacity:0;pointer-events:none;font-size:16px';
|
|
(active?.closest('[role="dialog"]') ?? document.body).append(field);
|
|
try {
|
|
field.focus({ preventScroll: true });
|
|
field.select();
|
|
field.setSelectionRange(0, text.length);
|
|
if (!document.execCommand('copy')) {
|
|
throw new Error('Clipboard access unavailable');
|
|
}
|
|
} finally {
|
|
field.remove();
|
|
if (active instanceof HTMLElement) {
|
|
active.focus({ preventScroll: true });
|
|
}
|
|
if (selection) {
|
|
selection.removeAllRanges();
|
|
ranges.forEach((range) => selection.addRange(range));
|
|
}
|
|
if (
|
|
inputSelection &&
|
|
inputSelection[0] !== null &&
|
|
inputSelection[1] !== null &&
|
|
(active instanceof HTMLInputElement ||
|
|
active instanceof HTMLTextAreaElement)
|
|
) {
|
|
active.setSelectionRange(
|
|
inputSelection[0],
|
|
inputSelection[1],
|
|
inputSelection[2] ?? undefined,
|
|
);
|
|
}
|
|
}
|
|
}
|