Avoid false clipboard success on insecure origins
This commit is contained in:
@@ -131,9 +131,11 @@ function getWorkReferenceImages(work: GalleryWork): string[] {
|
||||
}
|
||||
|
||||
async function copyGalleryText(text: string, successMessage: string) {
|
||||
const copied = await copyTextToClipboard(text);
|
||||
if (copied) {
|
||||
const copyResult = await copyTextToClipboard(text);
|
||||
if (copyResult === 'copied') {
|
||||
toast.success(successMessage);
|
||||
} else if (copyResult === 'manual') {
|
||||
toast.info('已选中文本,请按 Ctrl+C 复制');
|
||||
} else {
|
||||
toast.error('复制失败,请手动选择文本复制');
|
||||
}
|
||||
|
||||
@@ -82,9 +82,11 @@ const IMAGE_TO_IMAGE_DRAFT_KEY = 'miaojing:image-to-image-draft';
|
||||
async function copyText(value: string) {
|
||||
const text = value.trim();
|
||||
if (!text) return;
|
||||
const copied = await copyTextToClipboard(text);
|
||||
if (copied) {
|
||||
const copyResult = await copyTextToClipboard(text);
|
||||
if (copyResult === 'copied') {
|
||||
toast.success('已复制');
|
||||
} else if (copyResult === 'manual') {
|
||||
toast.info('已选中文本,请按 Ctrl+C 复制');
|
||||
} else {
|
||||
toast.error('复制失败,请手动选择文本复制');
|
||||
}
|
||||
|
||||
@@ -175,9 +175,11 @@ export function CreationDetailDialog({ record, open, onClose, onPublishChange }:
|
||||
|
||||
const handleCopyPrompt = async () => {
|
||||
if (!record.prompt) return;
|
||||
const copied = await copyTextToClipboard(record.prompt);
|
||||
if (copied) {
|
||||
const copyResult = await copyTextToClipboard(record.prompt);
|
||||
if (copyResult === 'copied') {
|
||||
toast.success('提示词已复制');
|
||||
} else if (copyResult === 'manual') {
|
||||
toast.info('已选中提示词,请按 Ctrl+C 复制');
|
||||
} else {
|
||||
toast.error('复制失败,请手动选择文本复制');
|
||||
}
|
||||
@@ -185,9 +187,11 @@ export function CreationDetailDialog({ record, open, onClose, onPublishChange }:
|
||||
|
||||
const handleCopyNegativePrompt = async () => {
|
||||
if (!record.negativePrompt) return;
|
||||
const copied = await copyTextToClipboard(record.negativePrompt);
|
||||
if (copied) {
|
||||
const copyResult = await copyTextToClipboard(record.negativePrompt);
|
||||
if (copyResult === 'copied') {
|
||||
toast.success('反向提示词已复制');
|
||||
} else if (copyResult === 'manual') {
|
||||
toast.info('已选中反向提示词,请按 Ctrl+C 复制');
|
||||
} else {
|
||||
toast.error('复制失败,请手动选择文本复制');
|
||||
}
|
||||
|
||||
162
src/lib/utils.ts
162
src/lib/utils.ts
@@ -5,74 +5,120 @@ export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export async function copyTextToClipboard(text: string): Promise<boolean> {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return false;
|
||||
export type ClipboardCopyResult = 'copied' | 'manual' | 'failed';
|
||||
|
||||
function openManualCopyDialog(value: string): ClipboardCopyResult {
|
||||
const existing = document.getElementById('miaojing-manual-copy-dialog');
|
||||
existing?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'miaojing-manual-copy-dialog';
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.inset = '0';
|
||||
overlay.style.zIndex = '2147483647';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.padding = '24px';
|
||||
overlay.style.background = 'rgba(16, 12, 8, 0.52)';
|
||||
overlay.style.backdropFilter = 'blur(8px)';
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.style.width = 'min(720px, 100%)';
|
||||
panel.style.maxHeight = 'min(620px, 90vh)';
|
||||
panel.style.display = 'flex';
|
||||
panel.style.flexDirection = 'column';
|
||||
panel.style.gap = '12px';
|
||||
panel.style.padding = '18px';
|
||||
panel.style.borderRadius = '18px';
|
||||
panel.style.border = '1px solid rgba(120, 82, 38, 0.22)';
|
||||
panel.style.background = 'rgba(255, 252, 246, 0.98)';
|
||||
panel.style.boxShadow = '0 24px 80px rgba(0, 0, 0, 0.28)';
|
||||
panel.style.color = '#24170f';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = '浏览器限制了自动复制';
|
||||
title.style.fontSize = '16px';
|
||||
title.style.fontWeight = '700';
|
||||
|
||||
const help = document.createElement('div');
|
||||
help.textContent = '下面的提示词已自动选中,请按 Ctrl+C 复制;Mac 请按 Command+C。';
|
||||
help.style.fontSize = '13px';
|
||||
help.style.lineHeight = '1.6';
|
||||
help.style.color = '#6f5f4d';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = value;
|
||||
textarea.readOnly = true;
|
||||
textarea.style.width = '100%';
|
||||
textarea.style.minHeight = '260px';
|
||||
textarea.style.maxHeight = '46vh';
|
||||
textarea.style.resize = 'vertical';
|
||||
textarea.style.padding = '12px';
|
||||
textarea.style.borderRadius = '12px';
|
||||
textarea.style.border = '1px solid rgba(120, 82, 38, 0.22)';
|
||||
textarea.style.background = '#fffaf2';
|
||||
textarea.style.color = '#24170f';
|
||||
textarea.style.fontSize = '13px';
|
||||
textarea.style.lineHeight = '1.65';
|
||||
textarea.style.outline = 'none';
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.style.display = 'flex';
|
||||
actions.style.justifyContent = 'flex-end';
|
||||
actions.style.gap = '10px';
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.type = 'button';
|
||||
closeButton.textContent = '关闭';
|
||||
closeButton.style.height = '36px';
|
||||
closeButton.style.padding = '0 14px';
|
||||
closeButton.style.borderRadius = '10px';
|
||||
closeButton.style.border = '1px solid rgba(120, 82, 38, 0.22)';
|
||||
closeButton.style.background = '#ffffff';
|
||||
closeButton.style.color = '#24170f';
|
||||
closeButton.style.cursor = 'pointer';
|
||||
|
||||
const close = () => {
|
||||
window.removeEventListener('keydown', handleKeyDown, true);
|
||||
overlay.remove();
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') close();
|
||||
};
|
||||
closeButton.addEventListener('click', close);
|
||||
window.addEventListener('keydown', handleKeyDown, true);
|
||||
|
||||
actions.appendChild(closeButton);
|
||||
panel.append(title, help, textarea, actions);
|
||||
overlay.appendChild(panel);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
window.setTimeout(() => {
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, value.length);
|
||||
}, 0);
|
||||
|
||||
return 'manual';
|
||||
}
|
||||
|
||||
export async function copyTextToClipboard(text: string): Promise<ClipboardCopyResult> {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return 'failed';
|
||||
|
||||
const value = text.trim();
|
||||
if (!value) return false;
|
||||
if (!value) return 'failed';
|
||||
|
||||
if (navigator.clipboard?.writeText && window.isSecureContext) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
return 'copied';
|
||||
} catch {
|
||||
// Fall back below. Some browsers expose the API but still reject it.
|
||||
return openManualCopyDialog(value);
|
||||
}
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = value;
|
||||
textarea.setAttribute('aria-hidden', 'true');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '12px';
|
||||
textarea.style.top = '12px';
|
||||
textarea.style.width = '2px';
|
||||
textarea.style.height = '2px';
|
||||
textarea.style.padding = '0';
|
||||
textarea.style.border = '0';
|
||||
textarea.style.outline = '0';
|
||||
textarea.style.boxShadow = 'none';
|
||||
textarea.style.background = 'transparent';
|
||||
textarea.style.color = 'transparent';
|
||||
textarea.style.caretColor = 'transparent';
|
||||
textarea.style.zIndex = '-1';
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
const selection = document.getSelection();
|
||||
const previousRange = selection && selection.rangeCount > 0 ? selection.getRangeAt(0).cloneRange() : null;
|
||||
const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
let cleanupScheduled = false;
|
||||
|
||||
try {
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, value.length);
|
||||
const copied = document.queryCommandSupported?.('copy') !== false && document.execCommand('copy');
|
||||
if (copied) {
|
||||
cleanupScheduled = true;
|
||||
window.setTimeout(() => {
|
||||
textarea.remove();
|
||||
previousFocus?.focus({ preventScroll: true });
|
||||
if (selection && previousRange) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(previousRange);
|
||||
}
|
||||
}, 80);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
if (!cleanupScheduled && document.body.contains(textarea)) {
|
||||
document.body.removeChild(textarea);
|
||||
previousFocus?.focus({ preventScroll: true });
|
||||
if (selection && previousRange) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(previousRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
return openManualCopyDialog(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user