Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 46 additions & 25 deletions src/lib/clipboard.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,62 @@
// useClipboard — navigator.clipboard.writeText with an execCommand fallback.
// Lifted from the host (apps/web/composables/useClipboard.ts, now a re-export
// shim). SSR/touch-safe: reports unsupported instead of throwing; copyText
// resolves false on any failure so callers can fall back to manual selection.
const focusTrapContainerSelector = '[role="dialog"], [role="alertdialog"]'

function resolveFallbackContainer(documentRef: Document): HTMLElement {
const activeElement = documentRef.activeElement
if (activeElement instanceof HTMLElement) {
return activeElement.closest<HTMLElement>(focusTrapContainerSelector) ?? documentRef.body
}
return documentRef.body
}

function copyWithExecCommand(documentRef: Document, text: string): boolean {
if (typeof documentRef.execCommand !== 'function') return false

const previousFocus = documentRef.activeElement instanceof HTMLElement
? documentRef.activeElement
: null
const textArea = documentRef.createElement('textarea')
textArea.value = text
textArea.readOnly = true
textArea.tabIndex = -1
textArea.style.position = 'fixed'
textArea.style.left = '-9999px'
textArea.style.top = '0'
resolveFallbackContainer(documentRef).appendChild(textArea)

try {
textArea.focus()
textArea.select()
return documentRef.execCommand('copy')
} catch {
return false
} finally {
textArea.remove()
previousFocus?.focus()
}
}

// SSR/touch-safe clipboard access. The legacy fallback stays inside an active
// dialog so focus traps cannot steal its temporary textarea selection.
export function useClipboard() {
const hasNavigatorClipboard = typeof navigator !== 'undefined' && !!navigator.clipboard?.writeText
const hasExecCommandFallback = typeof document !== 'undefined' && typeof document.execCommand === 'function'
const isSupported = hasNavigatorClipboard || hasExecCommandFallback

async function copyText(text: string): Promise<boolean> {
if (!isSupported) {
return false
}
if (!isSupported) return false

try {
if (hasNavigatorClipboard) {
if (hasNavigatorClipboard && typeof navigator !== 'undefined') {
try {
await navigator.clipboard.writeText(text)
return true
}

if (!hasExecCommandFallback || typeof document === 'undefined') {
catch {
return false
}

const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'fixed'
textArea.style.left = '-9999px'
textArea.style.top = '0'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
const success = document.execCommand('copy')
document.body.removeChild(textArea)
return success
} catch {
return false
}

if (!hasExecCommandFallback || typeof document === 'undefined') return false
return copyWithExecCommand(document, text)
}

return {
Expand Down
Loading