feat: refine codex live chat context flows
This commit is contained in:
117
src/utils/clipboard.ts
Normal file
117
src/utils/clipboard.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
type SelectionSnapshot = {
|
||||
activeElement: Element | null;
|
||||
ranges: Range[];
|
||||
};
|
||||
|
||||
function captureSelectionSnapshot(): SelectionSnapshot | null {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
const ranges: Range[] = [];
|
||||
|
||||
if (selection) {
|
||||
for (let index = 0; index < selection.rangeCount; index += 1) {
|
||||
ranges.push(selection.getRangeAt(index).cloneRange());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeElement: document.activeElement,
|
||||
ranges,
|
||||
};
|
||||
}
|
||||
|
||||
function restoreSelectionSnapshot(snapshot: SelectionSnapshot | null) {
|
||||
if (!snapshot || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
snapshot.ranges.forEach((range) => selection?.addRange(range));
|
||||
|
||||
const target = snapshot.activeElement;
|
||||
|
||||
if (target instanceof HTMLElement || target instanceof SVGElement) {
|
||||
try {
|
||||
target.focus({ preventScroll: true });
|
||||
} catch {
|
||||
target.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyUsingTextareaFallback(text: string) {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof document === 'undefined' ||
|
||||
typeof document.execCommand !== 'function' ||
|
||||
!document.body
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const snapshot = captureSelectionSnapshot();
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.tabIndex = -1;
|
||||
textarea.readOnly = true;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.setAttribute('aria-hidden', 'true');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.top = '0';
|
||||
textarea.style.left = '0';
|
||||
textarea.style.width = '1px';
|
||||
textarea.style.height = '1px';
|
||||
textarea.style.padding = '0';
|
||||
textarea.style.border = '0';
|
||||
textarea.style.outline = '0';
|
||||
textarea.style.boxShadow = 'none';
|
||||
textarea.style.background = 'transparent';
|
||||
textarea.style.opacity = '0';
|
||||
textarea.style.pointerEvents = 'none';
|
||||
textarea.style.fontSize = '16px';
|
||||
textarea.style.whiteSpace = 'pre';
|
||||
textarea.style.userSelect = 'text';
|
||||
textarea.style.webkitUserSelect = 'text';
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
try {
|
||||
try {
|
||||
textarea.focus({ preventScroll: true });
|
||||
} catch {
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
textarea.select();
|
||||
textarea.selectionStart = 0;
|
||||
textarea.selectionEnd = text.length;
|
||||
textarea.setSelectionRange(0, text.length);
|
||||
return document.execCommand('copy');
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
textarea.remove();
|
||||
restoreSelectionSnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyTextToClipboard(text: string): Promise<void> {
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
} catch {
|
||||
// Fall back when the browser exposes the API but rejects the write.
|
||||
}
|
||||
}
|
||||
|
||||
if (copyUsingTextareaFallback(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('브라우저가 클립보드 복사를 차단했습니다.');
|
||||
}
|
||||
Reference in New Issue
Block a user