mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 03:16:40 +00:00
Fix copy button in Assistant embed (RND-11876)
Harden the code-block copy button so it works in the Assistant embed. The
async Clipboard API can be unavailable or reject in cross-origin iframe /
non-secure contexts, which threw an unhandled rejection and left the button
showing a false success. Fall back to execCommand('copy') and only show the
copied state when the write actually succeeds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxDckjBnKCnQDGnuWufeKp
This commit is contained in:
@@ -6,7 +6,7 @@ import { Button } from '@/components/primitives';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getCodeTextFromId } from './utils';
|
||||
import { copyToClipboard, getCodeTextFromId } from './utils';
|
||||
|
||||
/**
|
||||
* Client component to copy the code of a code block.
|
||||
@@ -32,15 +32,17 @@ export function CopyCodeButton(props: { codeId: string; style: ClassValue }) {
|
||||
};
|
||||
}, [copied]);
|
||||
|
||||
const onClick = () => {
|
||||
const onClick = async () => {
|
||||
const codeText = getCodeTextFromId(codeId);
|
||||
if (codeText === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(codeText);
|
||||
|
||||
setCopied(true);
|
||||
// Only surface the "copied" state when the write actually succeeded, so a blocked
|
||||
// clipboard in the embed no longer looks like a successful copy.
|
||||
if (await copyToClipboard(codeText)) {
|
||||
setCopied(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -75,6 +75,61 @@ export function getCodeTextFromId(codeId: string): string | null {
|
||||
return element ? getCodeText(element) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to the clipboard in a way that survives embed contexts.
|
||||
*
|
||||
* The async Clipboard API (`navigator.clipboard`) is unavailable or rejects in some embed
|
||||
* setups — cross-origin iframes where the `clipboard-write` permission wasn't delegated, or
|
||||
* non-secure contexts — which is why copying silently failed (and logged an error) in the
|
||||
* Assistant embed. We fall back to a hidden `<textarea>` + `execCommand('copy')` so the button
|
||||
* keeps working there. Returns whether the copy succeeded so callers only show a success state
|
||||
* when the text actually reached the clipboard.
|
||||
*/
|
||||
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch {
|
||||
// The async API can reject in restricted embed contexts; fall back below.
|
||||
}
|
||||
}
|
||||
|
||||
return copyWithExecCommand(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy clipboard write used as a fallback when the async Clipboard API is unavailable.
|
||||
*
|
||||
* `execCommand('copy')` still works in cross-origin iframes where the async API is blocked,
|
||||
* which is why we keep it as the safety net for the embed.
|
||||
*/
|
||||
function copyWithExecCommand(text: string): boolean {
|
||||
if (typeof document === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
// Keep it out of view and avoid scrolling/zooming to it when focused.
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.top = '0';
|
||||
textarea.style.left = '0';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
try {
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
return document.execCommand('copy');
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
textarea.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the code text from the DOM,
|
||||
* ignoring the empty white space we use for empty lines (represented with a class "ew").
|
||||
|
||||
Reference in New Issue
Block a user