import { useCallback, useMemo, useState } from 'react'; import { newClipboardItem, read, readText, write, writeText, } from './clipboard.js'; import type { ClipboardMode } from './types.js'; interface UseClipboardReturn { /** * polyfill native api (stable) */ /** * Fallback on 'read' mode */ read: () => Promise; /** * Fallback on 'readText' mode */ readText: () => Promise; /** * Fallback on 'write' mode with blobs (get blob for each first type for each item) state */ write: (data: ClipboardItems) => Promise; /** * Fallback on 'writeText' mode with text state */ writeText: (data: string) => Promise; /** * custom helper (stable) */ /** * use write but no need do to manually ClipboardItems transform * Fallback on 'writeText' mode with text state * @param data * @param type a mime type default to 'text/plain' */ rawWriteWithType: ( data: string, type?: 'text/html' | 'text/plain' | string, ) => Promise; /** * state for alternative */ /** * state to know if you need to display an alternative interface */ shouldFallback: ClipboardMode | undefined; /** * state for 'writeText' / 'rawWriteWithType' mode fallback */ text: string | undefined; /** * state for 'write' mode fallback */ blobs: Blob[] | undefined; /** * method to clean up state for alternative interface (set shouldFallback to undefined, text to undefined and blobs to undefined) * stable */ cleanShouldFallback: () => void; } /** * Give you a wrapped clipboard api and a shouldFallback state. * ignore if read and readText return promise filled with undefined, and rely on shouldFallback to display an alternative interaction. * You should use ClipboardFallback component * * @example * ```tsx * const { readText, shouldFallback, setShouldFallback } = useClipboard(); * * function handlePasteAction() { * // yes you can safely ignore error here, * // readText() will never throw, error is caught and set shouldFallback to corresponding mode * void readText().then(handlePaste); * } * * function handlePaste(text: string | undefined) { * if (!text) return; * * setState(text); * setShouldFallback(null); * } * * return ( * <> * * setShouldFallback(null)} * onReadText={handlePaste} * /> * * ) * ``` */ export function useClipboard(): UseClipboardReturn { const [shouldFallback, setShouldFallback] = useState< ClipboardMode | undefined >(); const [text, setText] = useState(); const [blobs, setBlobs] = useState(); const cleanShouldFallback = useCallback(() => { setShouldFallback(undefined); setText(undefined); setBlobs(undefined); }, []); const clipboardAPI = useMemo( () => ({ /** * Attempt to read clipboard items. Sets the fallback and returns `null` if the attempt failed. */ async read(): Promise { try { return await read(); } catch { setShouldFallback('read'); return null; } }, /** * Attempt to read text from the clipboard. Sets the fallback and returns `null` if the attempt failed. */ async readText(): Promise { try { return await readText(); } catch { setShouldFallback('readText'); return null; } }, async write(data: ClipboardItems): Promise { try { return await write(data); } catch { const blobs = await Promise.all( data.map((ci) => ci.getType(ci.types[0])), ); setBlobs(blobs); setShouldFallback('write'); } }, async rawWriteWithType(data: string, type = 'text/plain') { try { const item = await newClipboardItem({ [type]: new Blob([data], { type }), }); return await write([item]); } catch { setText(data); setShouldFallback('writeText'); } }, async writeText(data: string) { try { return await writeText(data); } catch { setText(data); setShouldFallback('writeText'); } }, }), [], ); return { ...clipboardAPI, shouldFallback, text, blobs, cleanShouldFallback, }; }