useClipboard
A hook to copy text to clipboard
Loading...
Installation
npx shadcn@latest add @hooks/use-clipboardpnpm dlx shadcn@latest add @hooks/use-clipboardyarn dlx shadcn@latest add @hooks/use-clipboardbun x shadcn@latest add @hooks/use-clipboardCopy and paste the following code into your project.
import { useEffectWithTarget } from '@/registry/hooks/use-effect-with-target'
import { useLatest } from '@/registry/hooks/use-latest'
import { getTargetElement } from '@/registry/lib/create-effect-with-target'
import type { BasicTarget } from '@/registry/lib/create-effect-with-target'
type noop = (...p: any) => void
export type Target = BasicTarget<HTMLElement | Element | Window | Document>
interface Options<T extends Target = Target> {
target?: T
capture?: boolean
once?: boolean
passive?: boolean
enable?: boolean
}
function useEventListener<K extends keyof HTMLElementEventMap>(
eventName: K,
handler: (ev: HTMLElementEventMap[K]) => void,
options?: Options<HTMLElement>,
): void
function useEventListener<K extends keyof ElementEventMap>(
eventName: K,
handler: (ev: ElementEventMap[K]) => void,
options?: Options<Element>,
): void
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (ev: DocumentEventMap[K]) => void,
options?: Options<Document>,
): void
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (ev: WindowEventMap[K]) => void,
options?: Options<Window>,
): void
function useEventListener(
eventName: string | string[],
handler: (event: Event) => void,
options?: Options<Window>,
): void
function useEventListener(
eventName: string | string[],
handler: noop,
options: Options,
): void
function useEventListener(
eventName: string | string[],
handler: noop,
options: Options = {},
) {
const { enable = true } = options
const handlerRef = useLatest(handler)
useEffectWithTarget(
() => {
if (!enable) {
return
}
const targetElement = getTargetElement(options.target, window)
if (!targetElement?.addEventListener) {
return
}
const eventListener = (event: Event) => {
return handlerRef.current(event)
}
const eventNameArray = Array.isArray(eventName) ? eventName : [eventName]
eventNameArray.forEach((event) => {
targetElement.addEventListener(event, eventListener, {
capture: options.capture,
once: options.once,
passive: options.passive,
})
})
return () => {
eventNameArray.forEach((event) => {
targetElement.removeEventListener(event, eventListener, {
capture: options.capture,
})
})
}
},
[eventName, options.capture, options.once, options.passive, enable],
options.target,
)
}
export { useEventListener }import { useMemo, useRef } from 'react'
type noop = (this: any, ...args: any[]) => any
type PickFunction<T extends noop> = (
this: ThisParameterType<T>,
...args: Parameters<T>
) => ReturnType<T>
export function useMemoizedFn<T extends noop>(fn: T) {
const fnRef = useRef<T>(fn)
// why not write `fnRef.current = fn`?
// https://github.com/alibaba/hooks/issues/728
fnRef.current = useMemo<T>(() => fn, [fn])
const memoizedFn = useRef<PickFunction<T>>(undefined)
if (!memoizedFn.current) {
memoizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args)
}
}
return memoizedFn.current
}import { useEffect } from 'react'
import { useLatest } from '@/registry/hooks/use-latest'
export function useUnmount(fn: () => void) {
const fnRef = useLatest(fn)
useEffect(
() => () => {
fnRef.current()
},
[],
)
}/**
* A library that checks if the code is running in a browser.
*/
export const isBrowser = typeof window !== 'undefined'import { useEffect, useMemo, useRef, useState } from 'react'
import { useEventListener } from '@/registry/hooks/use-event-listener'
import { useMemoizedFn } from '@/registry/hooks/use-memoized-fn'
import { useUnmount } from '@/registry/hooks/use-unmount'
import { isBrowser } from '@/registry/lib/is-browser'
export interface UseClipboardOptions {
/**
* Enabled reading for clipboard
*
* @default false
*/
read?: boolean
/**
* Copy source
*/
source?: string
/**
* Milliseconds to reset state of `copied` ref
*
* @default 1500
*/
copiedDuring?: number
/**
* Whether fallback to document.execCommand('copy') if clipboard is undefined.
*
* @default false
*/
legacy?: boolean
}
export interface UseClipboardReturn {
isSupported: boolean
text: string
copied: boolean
copy: (text?: string) => Promise<void>
}
type PermissionState = 'granted' | 'denied' | 'prompt' | undefined
function isAllowed(status: PermissionState): boolean {
return status === 'granted' || status === 'prompt'
}
function legacyCopy(value: string): void {
const ta = document.createElement('textarea')
ta.value = value
ta.style.position = 'absolute'
ta.style.opacity = '0'
ta.setAttribute('readonly', '')
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
ta.remove()
}
function legacyRead(): string {
return document?.getSelection?.()?.toString() ?? ''
}
/**
* Reactive Clipboard API.
*
* @param options - Configuration options
* @returns Clipboard state and methods
*/
export function useClipboard(
options: UseClipboardOptions = {},
): UseClipboardReturn {
const { read = false, source, copiedDuring = 1500, legacy = false } = options
const [text, setText] = useState<string>('')
const [copied, setCopied] = useState<boolean>(false)
const [permissionRead, setPermissionRead] =
useState<PermissionState>(undefined)
const [permissionWrite, setPermissionWrite] =
useState<PermissionState>(undefined)
const timeoutRef = useRef<number | null>(null)
const isClipboardApiSupported = useMemo(() => {
if (!isBrowser) return false
return 'clipboard' in navigator
}, [])
const isSupported = useMemo(() => {
return isClipboardApiSupported || legacy
}, [isClipboardApiSupported, legacy])
// Check permissions
useEffect(() => {
if (!isBrowser || !isClipboardApiSupported) return
const checkPermissions = async () => {
try {
if ('permissions' in navigator) {
const readPermission = await navigator.permissions.query({
name: 'clipboard-read' as PermissionName,
})
setPermissionRead(readPermission.state)
readPermission.onchange = () => {
setPermissionRead(readPermission.state)
}
const writePermission = await navigator.permissions.query({
name: 'clipboard-write' as PermissionName,
})
setPermissionWrite(writePermission.state)
writePermission.onchange = () => {
setPermissionWrite(writePermission.state)
}
}
} catch {
// Permissions API might not be supported or clipboard permissions might not be queryable
// In this case, we'll try to use the clipboard API directly
}
}
checkPermissions()
}, [isClipboardApiSupported])
const updateText = useMemoizedFn(async () => {
let useLegacy = !(isClipboardApiSupported && isAllowed(permissionRead))
if (!useLegacy) {
try {
const clipboardText = await navigator.clipboard.readText()
setText(clipboardText)
} catch {
useLegacy = true
}
}
if (useLegacy) {
setText(legacyRead())
}
})
// Listen to copy/cut events if read is enabled
useEventListener(isSupported && read ? ['copy', 'cut'] : [], updateText, {
passive: true,
enable: isSupported && read,
})
const copy = useMemoizedFn(async (value?: string) => {
const textToCopy = value ?? source
if (!isSupported || textToCopy == null) return
let useLegacy = !(isClipboardApiSupported && isAllowed(permissionWrite))
if (!useLegacy) {
try {
await navigator.clipboard.writeText(textToCopy)
} catch {
useLegacy = true
}
}
if (useLegacy) {
legacyCopy(textToCopy)
}
setText(textToCopy)
setCopied(true)
// Clear existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Set new timeout
timeoutRef.current = window.setTimeout(() => {
setCopied(false)
timeoutRef.current = null
}, copiedDuring)
})
// Cleanup timeout on unmount
useUnmount(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
})
return {
isSupported,
text,
copied,
copy,
}
}API
export interface UseClipboardOptions {
/**
* Enabled reading for clipboard
*
* @default false
*/
read?: boolean
/**
* Copy source
*/
source?: string
/**
* Milliseconds to reset state of `copied` ref
*
* @default 1500
*/
copiedDuring?: number
/**
* Whether fallback to document.execCommand('copy') if clipboard is undefined.
*
* @default false
*/
legacy?: boolean
}
export interface UseClipboardReturn {
isSupported: boolean
text: string
copied: boolean
copy: (text?: string) => Promise<void>
}
type PermissionState = 'granted' | 'denied' | 'prompt' | undefined
/**
* A hook to copy text to clipboard
* @param options - The options for the clipboard
* @returns The clipboard state and methods
*/
export function useClipboard(options?: UseClipboardOptions): UseClipboardReturnCredits
Last updated on