188 lines · 4.9 KB · TypeScript
RawDownload
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 };}