Merge pull request #51 from bizzkoot/fix/copy-button-web
fix: copy button functionality in web browsers
This commit is contained in:
@@ -2,6 +2,7 @@ import { createSignal, onMount, Show, createEffect } from "solid-js"
|
|||||||
import type { Highlighter } from "shiki/bundle/full"
|
import type { Highlighter } from "shiki/bundle/full"
|
||||||
import { useTheme } from "../lib/theme"
|
import { useTheme } from "../lib/theme"
|
||||||
import { getSharedHighlighter, escapeHtml } from "../lib/markdown"
|
import { getSharedHighlighter, escapeHtml } from "../lib/markdown"
|
||||||
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
|
|
||||||
const inlineLoadedLanguages = new Set<string>()
|
const inlineLoadedLanguages = new Set<string>()
|
||||||
|
|
||||||
@@ -61,10 +62,12 @@ export function CodeBlockInline(props: CodeBlockInlineProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copyCode = async () => {
|
const copyCode = async () => {
|
||||||
await navigator.clipboard.writeText(props.code)
|
const success = await copyToClipboard(props.code)
|
||||||
|
if (success) {
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createEffect, createSignal, onMount, onCleanup } from "solid-js"
|
|||||||
import { renderMarkdown, onLanguagesLoaded, initMarkdown, decodeHtmlEntities } from "../lib/markdown"
|
import { renderMarkdown, onLanguagesLoaded, initMarkdown, decodeHtmlEntities } from "../lib/markdown"
|
||||||
import type { TextPart, RenderCache } from "../types/message"
|
import type { TextPart, RenderCache } from "../types/message"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
const markdownRenderCache = new Map<string, RenderCache>()
|
const markdownRenderCache = new Map<string, RenderCache>()
|
||||||
@@ -125,13 +126,20 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
const code = copyButton.getAttribute("data-code")
|
const code = copyButton.getAttribute("data-code")
|
||||||
if (code) {
|
if (code) {
|
||||||
const decodedCode = decodeURIComponent(code)
|
const decodedCode = decodeURIComponent(code)
|
||||||
await navigator.clipboard.writeText(decodedCode)
|
const success = await copyToClipboard(decodedCode)
|
||||||
const copyText = copyButton.querySelector(".copy-text")
|
const copyText = copyButton.querySelector(".copy-text")
|
||||||
if (copyText) {
|
if (copyText) {
|
||||||
|
if (success) {
|
||||||
copyText.textContent = "Copied!"
|
copyText.textContent = "Copied!"
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copyText.textContent = "Copy"
|
copyText.textContent = "Copy"
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
copyText.textContent = "Failed"
|
||||||
|
setTimeout(() => {
|
||||||
|
copyText.textContent = "Copy"
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { MessageInfo, ClientPart } from "../types/message"
|
|||||||
import { partHasRenderableText } from "../types/message"
|
import { partHasRenderableText } from "../types/message"
|
||||||
import type { MessageRecord } from "../stores/message-v2/types"
|
import type { MessageRecord } from "../stores/message-v2/types"
|
||||||
import MessagePart from "./message-part"
|
import MessagePart from "./message-part"
|
||||||
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
|
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
record: MessageRecord
|
record: MessageRecord
|
||||||
@@ -155,8 +156,8 @@ interface MessageItemProps {
|
|||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
const content = getRawContent()
|
const content = getRawContent()
|
||||||
if (!content) return
|
if (!content) return
|
||||||
await navigator.clipboard.writeText(content)
|
const success = await copyToClipboard(content)
|
||||||
setCopied(true)
|
setCopied(success)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { formatShortcut } from "../lib/keyboard-utils"
|
|||||||
import { showToastNotification } from "../lib/notifications"
|
import { showToastNotification } from "../lib/notifications"
|
||||||
import { deleteSession, loading, renameSession } from "../stores/sessions"
|
import { deleteSession, loading, renameSession } from "../stores/sessions"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
|
|
||||||
@@ -74,12 +75,12 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
const success = await copyToClipboard(sessionId)
|
||||||
throw new Error("Clipboard API unavailable")
|
if (success) {
|
||||||
}
|
|
||||||
|
|
||||||
await navigator.clipboard.writeText(sessionId)
|
|
||||||
showToastNotification({ message: "Session ID copied", variant: "success" })
|
showToastNotification({ message: "Session ID copied", variant: "success" })
|
||||||
|
} else {
|
||||||
|
showToastNotification({ message: "Unable to copy session ID", variant: "error" })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(`Failed to copy session ID ${sessionId}:`, error)
|
log.error(`Failed to copy session ID ${sessionId}:`, error)
|
||||||
showToastNotification({ message: "Unable to copy session ID", variant: "error" })
|
showToastNotification({ message: "Unable to copy session ID", variant: "error" })
|
||||||
|
|||||||
61
packages/ui/src/lib/clipboard.ts
Normal file
61
packages/ui/src/lib/clipboard.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Clipboard utility with fallback for non-secure contexts
|
||||||
|
* The modern Clipboard API requires HTTPS or localhost, but document.execCommand
|
||||||
|
* works in HTTP contexts as a fallback.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getLogger } from "./logger"
|
||||||
|
|
||||||
|
const log = getLogger("actions")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy text to clipboard with fallback for non-secure contexts
|
||||||
|
* @param text - The text to copy
|
||||||
|
* @returns Promise<boolean> - true if successful, false if failed
|
||||||
|
*/
|
||||||
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Try modern Clipboard API first (requires secure context)
|
||||||
|
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
log.info("Copied text using Clipboard API")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.warn("Clipboard API failed, trying fallback:", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for non-secure contexts (HTTP) using document.execCommand
|
||||||
|
try {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
log.error("Document not available for clipboard fallback")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary textarea element
|
||||||
|
const textArea = document.createElement("textarea")
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = "fixed"
|
||||||
|
textArea.style.left = "-9999px"
|
||||||
|
textArea.style.top = "-9999px"
|
||||||
|
textArea.style.opacity = "0"
|
||||||
|
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
|
||||||
|
const success = document.execCommand("copy")
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
log.info("Copied text using execCommand fallback")
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
log.error("execCommand copy failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Clipboard fallback failed:", error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user