useClipboard
Menyalin ke papan klip, dengan penanda tersalin yang padam sendiri.
Clipboard API hanya tersedia di konteks aman (HTTPS atau localhost).
Menyalin ke papan klip, dengan penanda "tersalin" yang padam sendiri.
Jalur lamanya — document.execCommand('copy') lewat <textarea> tersembunyi — sengaja dipertahankan di balik opsi legacy, dan bukan karena peramban tua. navigator.clipboard hanya ada di konteks aman: HTTPS atau localhost. Tombol salin yang bekerja sempurna di mesin sendiri dan diam saja di jaringan kantor lewat HTTP adalah cacat yang membingungkan justru karena tidak melempar apa-apa.
copied padam sendiri setelah copiedDuring, dan timernya dibatalkan bersama scope.
Pemasangan
npx shadcn-vue@latest add @kliping/use-clipboard Perlu @kliping terdaftar di components.json project Anda. Lihat cara menyiapkannya.
Ikut terpasang sendiri: is-browser, use-event-listener.
Salin dulu is-browser, target, use-event-listener — masing-masing memuat sumbernya di halamannya sendiri — lalu tempel berkas ini.
import { getCurrentScope, onScopeDispose, readonly, ref, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { isBrowser } from '@/lib/is-browser'
import { useEventListener } from '@/composables/use-event-listener'
export interface UseClipboardOptions {
/** Ikuti isi papan klip saat orang menyalin di tempat lain. Bawaan: false. */
read?: boolean
/** Teks bawaan yang disalin kalau `copy()` dipanggil tanpa argumen. */
source?: MaybeRefOrGetter<string | undefined>
/** Berapa lama `copied` menyala, dalam milidetik. Bawaan: 1500. */
copiedDuring?: number
/** Pakai jalur `execCommand` kalau Clipboard API tidak tersedia. Bawaan: false. */
legacy?: boolean
}
function salinCaraLama(nilai: string) {
const kotak = document.createElement('textarea')
kotak.value = nilai
kotak.style.position = 'absolute'
kotak.style.opacity = '0'
kotak.setAttribute('readonly', '')
document.body.appendChild(kotak)
kotak.select()
document.execCommand('copy')
kotak.remove()
}
export function useClipboard(options: UseClipboardOptions = {}) {
const { read = false, source, copiedDuring = 1500, legacy = false } = options
const adaApi = isBrowser && 'clipboard' in navigator
const isSupported = ref(adaApi || legacy)
const text = ref('')
const copied = ref(false)
let timer: ReturnType<typeof setTimeout> | undefined
const bacaPapanKlip = async () => {
if (adaApi) {
try {
text.value = await navigator.clipboard.readText()
return
}
catch {
// Izin ditolak atau bukan konteks aman — jatuh ke jalur lama.
}
}
text.value = document?.getSelection?.()?.toString() ?? ''
}
useEventListener(['copy', 'cut'], bacaPapanKlip, {
target: isBrowser ? document : undefined,
passive: true,
enable: () => isSupported.value && read,
})
const copy = async (nilai?: string) => {
const isi = nilai ?? toValue(source)
if (!isSupported.value || isi == null)
return
let pakaiCaraLama = !adaApi
if (!pakaiCaraLama) {
try {
await navigator.clipboard.writeText(isi)
}
catch {
pakaiCaraLama = true
}
}
if (pakaiCaraLama && legacy)
salinCaraLama(isi)
text.value = isi
copied.value = true
if (timer !== undefined)
clearTimeout(timer)
timer = setTimeout(() => {
copied.value = false
timer = undefined
}, copiedDuring)
}
if (getCurrentScope()) {
onScopeDispose(() => {
if (timer !== undefined)
clearTimeout(timer)
})
}
return { isSupported: readonly(isSupported), text, copied: readonly(copied), copy }
}API
export interface UseClipboardOptions {
/** Ikuti isi papan klip saat orang menyalin di tempat lain. Bawaan: false. */
read?: boolean
/** Teks bawaan yang disalin kalau `copy()` dipanggil tanpa argumen. */
source?: MaybeRefOrGetter<string | undefined>
/** Berapa lama `copied` menyala, dalam milidetik. Bawaan: 1500. */
copiedDuring?: number
/** Pakai jalur `execCommand` kalau Clipboard API tidak tersedia. Bawaan: false. */
legacy?: boolean
}
export function useClipboard(options: UseClipboardOptions = {})