import * as React from "react";import * as Schema from "effect/Schema"; export class ClipboardApiUnavailableError extends Schema.TaggedError<ClipboardApiUnavailableError>()( "ClipboardApiUnavailableError", { target: Schema.String, },) { override get message(): string { return `Clipboard API is unavailable while copying ${this.target}.`; }} export class ClipboardWriteError extends Schema.TaggedError<ClipboardWriteError>()( "ClipboardWriteError", { target: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to copy ${this.target} to the clipboard.`; }} export class ClipboardReadUnavailableError extends Schema.TaggedError<ClipboardReadUnavailableError>()( "ClipboardReadUnavailableError", { target: Schema.String, },) { override get message(): string { return `Clipboard API is unavailable while reading ${this.target}.`; }} export class ClipboardReadError extends Schema.TaggedError<ClipboardReadError>()( "ClipboardReadError", { target: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to read ${this.target} from the clipboard.`; }} /** Copy fallback for remote web pages served over plain HTTP. */function writeTextWithExecCommand(value: string): boolean { if (typeof document === "undefined" || typeof document.execCommand !== "function") return false; const textarea = document.createElement("textarea"); textarea.value = value; textarea.setAttribute("readonly", ""); textarea.setAttribute("aria-hidden", "true"); textarea.style.position = "fixed"; textarea.style.top = "0"; textarea.style.left = "0"; textarea.style.opacity = "0"; textarea.style.fontSize = "16px"; const previouslyFocused = document.activeElement; document.body.appendChild(textarea); try { textarea.focus({ preventScroll: true }); textarea.select(); textarea.setSelectionRange(0, value.length); return document.execCommand("copy"); } catch { return false; } finally { textarea.remove(); const restoreFocus = (previouslyFocused as { focus?: unknown } | null)?.focus; if (typeof restoreFocus === "function") { restoreFocus.call(previouslyFocused); } }} export async function writeTextToClipboard(value: string, target = "text") { if (typeof window === "undefined") { throw new ClipboardApiUnavailableError({ target, }); } if (!value) return false; if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) { if (writeTextWithExecCommand(value)) return true; throw new ClipboardApiUnavailableError({ target, }); } try { await navigator.clipboard.writeText(value); return true; } catch (cause) { throw new ClipboardWriteError({ target, cause, }); }} export async function readTextFromClipboard(target = "text"): Promise<string> { if ( typeof window === "undefined" || typeof navigator === "undefined" || !navigator.clipboard?.readText ) { throw new ClipboardReadUnavailableError({ target, }); } try { return await navigator.clipboard.readText(); } catch (cause) { throw new ClipboardReadError({ target, cause, }); }} export function useCopyToClipboard<TContext = void>({ timeout = 2000, target = "text", onCopy, onError,}: { timeout?: number; target?: string; onCopy?: (ctx: TContext) => void; onError?: (error: Error, ctx: TContext) => void;} = {}): { copyToClipboard: (value: string, ctx: TContext) => void; isCopied: boolean } { const [isCopied, setIsCopied] = React.useState(false); const timeoutIdRef = React.useRef<NodeJS.Timeout | null>(null); const onCopyRef = React.useRef(onCopy); const onErrorRef = React.useRef(onError); const targetRef = React.useRef(target); const timeoutRef = React.useRef(timeout); onCopyRef.current = onCopy; onErrorRef.current = onError; targetRef.current = target; timeoutRef.current = timeout; const copyToClipboard = React.useCallback((value: string, ctx: TContext): void => { void writeTextToClipboard(value, targetRef.current).then( (didCopy) => { if (!didCopy) return; if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); } setIsCopied(true); onCopyRef.current?.(ctx); if (timeoutRef.current !== 0) { timeoutIdRef.current = setTimeout(() => { setIsCopied(false); timeoutIdRef.current = null; }, timeoutRef.current); } }, (error) => { console.error(error); onErrorRef.current?.(error, ctx); }, ); }, []); // Cleanup timeout on unmount React.useEffect(() => { return (): void => { if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); } }; }, []); return { copyToClipboard, isCopied };}
import * as React from "react";
import * as Schema from "effect/Schema";
export class ClipboardApiUnavailableError extends Schema.TaggedError<ClipboardApiUnavailableError>()(
"ClipboardApiUnavailableError",
{
target: Schema.String,
},
) {
override get message(): string {
return `Clipboard API is unavailable while copying ${this.target}.`;
}
}
export class ClipboardWriteError extends Schema.TaggedError<ClipboardWriteError>()(
"ClipboardWriteError",
{
target: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to copy ${this.target} to the clipboard.`;
}
}
export class ClipboardReadUnavailableError extends Schema.TaggedError<ClipboardReadUnavailableError>()(
"ClipboardReadUnavailableError",
{
target: Schema.String,
},
) {
override get message(): string {
return `Clipboard API is unavailable while reading ${this.target}.`;
}
}
export class ClipboardReadError extends Schema.TaggedError<ClipboardReadError>()(
"ClipboardReadError",
{
target: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to read ${this.target} from the clipboard.`;
}
}
/** Copy fallback for remote web pages served over plain HTTP. */
function writeTextWithExecCommand(value: string): boolean {
if (typeof document === "undefined" || typeof document.execCommand !== "function") return false;
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "");
textarea.setAttribute("aria-hidden", "true");
textarea.style.position = "fixed";
textarea.style.top = "0";
textarea.style.left = "0";
textarea.style.opacity = "0";
textarea.style.fontSize = "16px";
const previouslyFocused = document.activeElement;
document.body.appendChild(textarea);
try {
textarea.focus({ preventScroll: true });
textarea.select();
textarea.setSelectionRange(0, value.length);
return document.execCommand("copy");
} catch {
return false;
} finally {
textarea.remove();
const restoreFocus = (previouslyFocused as { focus?: unknown } | null)?.focus;
if (typeof restoreFocus === "function") {
restoreFocus.call(previouslyFocused);
}
}
}
export async function writeTextToClipboard(value: string, target = "text") {
if (typeof window === "undefined") {
throw new ClipboardApiUnavailableError({
target,
});
}
if (!value) return false;
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
if (writeTextWithExecCommand(value)) return true;
throw new ClipboardApiUnavailableError({
target,
});
}
try {
await navigator.clipboard.writeText(value);
return true;
} catch (cause) {
throw new ClipboardWriteError({
target,
cause,
});
}
}
export async function readTextFromClipboard(target = "text"): Promise<string> {
if (
typeof window === "undefined" ||
typeof navigator === "undefined" ||
!navigator.clipboard?.readText
) {
throw new ClipboardReadUnavailableError({
target,
});
}
try {
return await navigator.clipboard.readText();
} catch (cause) {
throw new ClipboardReadError({
target,
cause,
});
}
}
export function useCopyToClipboard<TContext = void>({
timeout = 2000,
target = "text",
onCopy,
onError,
}: {
timeout?: number;
target?: string;
onCopy?: (ctx: TContext) => void;
onError?: (error: Error, ctx: TContext) => void;
} = {}): { copyToClipboard: (value: string, ctx: TContext) => void; isCopied: boolean } {
const [isCopied, setIsCopied] = React.useState(false);
const timeoutIdRef = React.useRef<NodeJS.Timeout | null>(null);
const onCopyRef = React.useRef(onCopy);
const onErrorRef = React.useRef(onError);
const targetRef = React.useRef(target);
const timeoutRef = React.useRef(timeout);
onCopyRef.current = onCopy;
onErrorRef.current = onError;
targetRef.current = target;
timeoutRef.current = timeout;
const copyToClipboard = React.useCallback((value: string, ctx: TContext): void => {
void writeTextToClipboard(value, targetRef.current).then(
(didCopy) => {
if (!didCopy) return;
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
setIsCopied(true);
onCopyRef.current?.(ctx);
if (timeoutRef.current !== 0) {
timeoutIdRef.current = setTimeout(() => {
setIsCopied(false);
timeoutIdRef.current = null;
}, timeoutRef.current);
}
},
(error) => {
console.error(error);
onErrorRef.current?.(error, ctx);
},
);
}, []);
// Cleanup timeout on unmount
React.useEffect(() => {
return (): void => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
return { copyToClipboard, isCopied };
}