LightRAG/lightrag_webui/src/utils/clipboard.ts
yangdx c358f83d2d Implement robust clipboard functionality with multiple fallback strategies
• Add comprehensive clipboard utility
• Support legacy browser environments
• Provide detailed user feedback
• Handle security context failures
2025-09-24 23:12:16 +08:00

198 lines
5.2 KiB
TypeScript

/**
* Robust clipboard utility with multiple fallback strategies
* Handles various browser environments and security contexts
*/
export interface CopyResult {
success: boolean;
method: 'clipboard-api' | 'execCommand' | 'manual-select' | 'fallback';
error?: string;
}
/**
* Copy text to clipboard with multiple fallback strategies
* @param text - Text to copy to clipboard
* @returns Promise<CopyResult> - Result object with success status and method used
*/
export async function copyToClipboard(text: string): Promise<CopyResult> {
if (!text || text.trim() === '') {
return {
success: false,
method: 'fallback',
error: 'No text provided'
};
}
// Strategy 1: Modern Clipboard API (preferred)
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try {
await navigator.clipboard.writeText(text);
return {
success: true,
method: 'clipboard-api'
};
} catch (error) {
console.warn('Clipboard API failed:', error);
// Continue to fallback methods
}
}
// Strategy 2: Legacy execCommand (for older browsers)
try {
const result = await copyWithExecCommand(text);
if (result.success) {
return result;
}
} catch (error) {
console.warn('execCommand failed:', error);
// Continue to fallback methods
}
// Strategy 3: Manual text selection (most compatible)
try {
const result = await copyWithManualSelection(text);
if (result.success) {
return result;
}
} catch (error) {
console.warn('Manual selection failed:', error);
}
// Strategy 4: Complete fallback - return error
return {
success: false,
method: 'fallback',
error: 'All copy methods failed. Please copy the text manually.'
};
}
/**
* Copy using legacy execCommand method
*/
async function copyWithExecCommand(text: string): Promise<CopyResult> {
return new Promise((resolve) => {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
textarea.style.opacity = '0';
textarea.setAttribute('readonly', '');
document.body.appendChild(textarea);
try {
textarea.select();
textarea.setSelectionRange(0, text.length);
const successful = document.execCommand('copy');
if (successful) {
resolve({
success: true,
method: 'execCommand'
});
} else {
resolve({
success: false,
method: 'execCommand',
error: 'execCommand returned false'
});
}
} catch (error) {
resolve({
success: false,
method: 'execCommand',
error: error instanceof Error ? error.message : 'execCommand failed'
});
} finally {
document.body.removeChild(textarea);
}
});
}
/**
* Copy using manual text selection method
*/
async function copyWithManualSelection(text: string): Promise<CopyResult> {
return new Promise((resolve) => {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
textarea.setAttribute('readonly', '');
textarea.setAttribute('tabindex', '-1');
document.body.appendChild(textarea);
try {
// Focus and select the text
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, text.length);
// Try to trigger copy event
const copyEvent = new ClipboardEvent('copy', {
clipboardData: new DataTransfer()
});
if (copyEvent.clipboardData) {
copyEvent.clipboardData.setData('text/plain', text);
document.dispatchEvent(copyEvent);
resolve({
success: true,
method: 'manual-select'
});
} else {
// Fallback: keep text selected for manual copy
resolve({
success: false,
method: 'manual-select',
error: 'Manual selection prepared, but automatic copy failed'
});
}
} catch (error) {
resolve({
success: false,
method: 'manual-select',
error: error instanceof Error ? error.message : 'Manual selection failed'
});
} finally {
// Clean up after a short delay to allow copy operation
setTimeout(() => {
if (document.body.contains(textarea)) {
document.body.removeChild(textarea);
}
}, 100);
}
});
}
/**
* Check if clipboard functionality is available
*/
export function isClipboardSupported(): boolean {
return !!(
(navigator.clipboard && typeof navigator.clipboard.writeText === 'function') ||
typeof document !== 'undefined'
);
}
/**
* Get the best available clipboard method
*/
export function getBestClipboardMethod(): 'clipboard-api' | 'execCommand' | 'manual-select' | 'none' {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
return 'clipboard-api';
}
if (typeof document !== 'undefined') {
return 'execCommand';
}
return 'none';
}