Skip to content

Commit ff4df78

Browse files
ShlomoSteptclaude
andcommitted
Add Clipboard API fallback for older browsers
Added a copyToClipboard() helper function that: 1. Uses the modern Clipboard API (navigator.clipboard.writeText) when available 2. Falls back to document.execCommand('copy') for older browsers 3. Returns a Promise for consistent handling Also added user-facing error feedback: - Button now shows "Failed" for 2 seconds on copy failure - Improves UX by informing users when copy doesn't work Updated both copy button implementations: - Dynamically added copy buttons on pre/tool-result elements - Cell header copy buttons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a3076ee commit ff4df78

File tree

5 files changed

+166
-10
lines changed

5 files changed

+166
-10
lines changed

src/claude_code_transcripts/__init__.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ def set_github_repo(repo: str | None) -> contextvars.Token[str | None]:
278278
_github_repo = repo
279279
return _github_repo_var.set(repo)
280280

281+
281282
# API constants
282283
API_BASE_URL = "https://api.anthropic.com/v1"
283284
ANTHROPIC_VERSION = "2023-06-01"
@@ -1865,6 +1866,33 @@ def render_message_with_tool_pairs(
18651866
"""
18661867

18671868
JS = """
1869+
// Clipboard helper with fallback for older browsers
1870+
function copyToClipboard(text) {
1871+
// Modern browsers: use Clipboard API
1872+
if (navigator.clipboard && navigator.clipboard.writeText) {
1873+
return navigator.clipboard.writeText(text);
1874+
}
1875+
// Fallback: use execCommand('copy')
1876+
return new Promise(function(resolve, reject) {
1877+
var textarea = document.createElement('textarea');
1878+
textarea.value = text;
1879+
textarea.style.position = 'fixed';
1880+
textarea.style.left = '-9999px';
1881+
textarea.style.top = '0';
1882+
textarea.setAttribute('readonly', '');
1883+
document.body.appendChild(textarea);
1884+
textarea.select();
1885+
try {
1886+
var success = document.execCommand('copy');
1887+
document.body.removeChild(textarea);
1888+
if (success) { resolve(); }
1889+
else { reject(new Error('execCommand copy failed')); }
1890+
} catch (err) {
1891+
document.body.removeChild(textarea);
1892+
reject(err);
1893+
}
1894+
});
1895+
}
18681896
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
18691897
const timestamp = el.getAttribute('data-timestamp');
18701898
const date = new Date(timestamp);
@@ -1909,7 +1937,7 @@ def render_message_with_tool_pairs(
19091937
copyBtn.addEventListener('click', function(e) {
19101938
e.stopPropagation();
19111939
const textToCopy = el.textContent.replace(/^Copy$/, '').trim();
1912-
navigator.clipboard.writeText(textToCopy).then(function() {
1940+
copyToClipboard(textToCopy).then(function() {
19131941
copyBtn.textContent = 'Copied!';
19141942
copyBtn.classList.add('copied');
19151943
setTimeout(function() {
@@ -1918,6 +1946,8 @@ def render_message_with_tool_pairs(
19181946
}, 2000);
19191947
}).catch(function(err) {
19201948
console.error('Failed to copy:', err);
1949+
copyBtn.textContent = 'Failed';
1950+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
19211951
});
19221952
});
19231953
el.appendChild(copyBtn);
@@ -1936,7 +1966,7 @@ def render_message_with_tool_pairs(
19361966
const content = cell.querySelector('.cell-content');
19371967
textToCopy = content.textContent.trim();
19381968
}
1939-
navigator.clipboard.writeText(textToCopy).then(function() {
1969+
copyToClipboard(textToCopy).then(function() {
19401970
btn.textContent = 'Copied!';
19411971
btn.classList.add('copied');
19421972
setTimeout(function() {
@@ -1945,6 +1975,8 @@ def render_message_with_tool_pairs(
19451975
}, 2000);
19461976
}).catch(function(err) {
19471977
console.error('Failed to copy cell:', err);
1978+
btn.textContent = 'Failed';
1979+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
19481980
});
19491981
});
19501982
// Keyboard accessibility

tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_index_html.html

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,33 @@ <h1>Claude Code transcript</h1>
664664
</script>
665665
</div>
666666
<script>
667+
// Clipboard helper with fallback for older browsers
668+
function copyToClipboard(text) {
669+
// Modern browsers: use Clipboard API
670+
if (navigator.clipboard && navigator.clipboard.writeText) {
671+
return navigator.clipboard.writeText(text);
672+
}
673+
// Fallback: use execCommand('copy')
674+
return new Promise(function(resolve, reject) {
675+
var textarea = document.createElement('textarea');
676+
textarea.value = text;
677+
textarea.style.position = 'fixed';
678+
textarea.style.left = '-9999px';
679+
textarea.style.top = '0';
680+
textarea.setAttribute('readonly', '');
681+
document.body.appendChild(textarea);
682+
textarea.select();
683+
try {
684+
var success = document.execCommand('copy');
685+
document.body.removeChild(textarea);
686+
if (success) { resolve(); }
687+
else { reject(new Error('execCommand copy failed')); }
688+
} catch (err) {
689+
document.body.removeChild(textarea);
690+
reject(err);
691+
}
692+
});
693+
}
667694
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
668695
const timestamp = el.getAttribute('data-timestamp');
669696
const date = new Date(timestamp);
@@ -708,7 +735,7 @@ <h1>Claude Code transcript</h1>
708735
copyBtn.addEventListener('click', function(e) {
709736
e.stopPropagation();
710737
const textToCopy = el.textContent.replace(/^Copy$/, '').trim();
711-
navigator.clipboard.writeText(textToCopy).then(function() {
738+
copyToClipboard(textToCopy).then(function() {
712739
copyBtn.textContent = 'Copied!';
713740
copyBtn.classList.add('copied');
714741
setTimeout(function() {
@@ -717,6 +744,8 @@ <h1>Claude Code transcript</h1>
717744
}, 2000);
718745
}).catch(function(err) {
719746
console.error('Failed to copy:', err);
747+
copyBtn.textContent = 'Failed';
748+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
720749
});
721750
});
722751
el.appendChild(copyBtn);
@@ -735,7 +764,7 @@ <h1>Claude Code transcript</h1>
735764
const content = cell.querySelector('.cell-content');
736765
textToCopy = content.textContent.trim();
737766
}
738-
navigator.clipboard.writeText(textToCopy).then(function() {
767+
copyToClipboard(textToCopy).then(function() {
739768
btn.textContent = 'Copied!';
740769
btn.classList.add('copied');
741770
setTimeout(function() {
@@ -744,6 +773,8 @@ <h1>Claude Code transcript</h1>
744773
}, 2000);
745774
}).catch(function(err) {
746775
console.error('Failed to copy cell:', err);
776+
btn.textContent = 'Failed';
777+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
747778
});
748779
});
749780
// Keyboard accessibility

tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_001_html.html

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,33 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
11581158

11591159
</div>
11601160
<script>
1161+
// Clipboard helper with fallback for older browsers
1162+
function copyToClipboard(text) {
1163+
// Modern browsers: use Clipboard API
1164+
if (navigator.clipboard && navigator.clipboard.writeText) {
1165+
return navigator.clipboard.writeText(text);
1166+
}
1167+
// Fallback: use execCommand('copy')
1168+
return new Promise(function(resolve, reject) {
1169+
var textarea = document.createElement('textarea');
1170+
textarea.value = text;
1171+
textarea.style.position = 'fixed';
1172+
textarea.style.left = '-9999px';
1173+
textarea.style.top = '0';
1174+
textarea.setAttribute('readonly', '');
1175+
document.body.appendChild(textarea);
1176+
textarea.select();
1177+
try {
1178+
var success = document.execCommand('copy');
1179+
document.body.removeChild(textarea);
1180+
if (success) { resolve(); }
1181+
else { reject(new Error('execCommand copy failed')); }
1182+
} catch (err) {
1183+
document.body.removeChild(textarea);
1184+
reject(err);
1185+
}
1186+
});
1187+
}
11611188
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
11621189
const timestamp = el.getAttribute('data-timestamp');
11631190
const date = new Date(timestamp);
@@ -1202,7 +1229,7 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
12021229
copyBtn.addEventListener('click', function(e) {
12031230
e.stopPropagation();
12041231
const textToCopy = el.textContent.replace(/^Copy$/, '').trim();
1205-
navigator.clipboard.writeText(textToCopy).then(function() {
1232+
copyToClipboard(textToCopy).then(function() {
12061233
copyBtn.textContent = 'Copied!';
12071234
copyBtn.classList.add('copied');
12081235
setTimeout(function() {
@@ -1211,6 +1238,8 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
12111238
}, 2000);
12121239
}).catch(function(err) {
12131240
console.error('Failed to copy:', err);
1241+
copyBtn.textContent = 'Failed';
1242+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
12141243
});
12151244
});
12161245
el.appendChild(copyBtn);
@@ -1229,7 +1258,7 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
12291258
const content = cell.querySelector('.cell-content');
12301259
textToCopy = content.textContent.trim();
12311260
}
1232-
navigator.clipboard.writeText(textToCopy).then(function() {
1261+
copyToClipboard(textToCopy).then(function() {
12331262
btn.textContent = 'Copied!';
12341263
btn.classList.add('copied');
12351264
setTimeout(function() {
@@ -1238,6 +1267,8 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
12381267
}, 2000);
12391268
}).catch(function(err) {
12401269
console.error('Failed to copy cell:', err);
1270+
btn.textContent = 'Failed';
1271+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
12411272
});
12421273
});
12431274
// Keyboard accessibility

tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_002_html.html

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,33 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
443443

444444
</div>
445445
<script>
446+
// Clipboard helper with fallback for older browsers
447+
function copyToClipboard(text) {
448+
// Modern browsers: use Clipboard API
449+
if (navigator.clipboard && navigator.clipboard.writeText) {
450+
return navigator.clipboard.writeText(text);
451+
}
452+
// Fallback: use execCommand('copy')
453+
return new Promise(function(resolve, reject) {
454+
var textarea = document.createElement('textarea');
455+
textarea.value = text;
456+
textarea.style.position = 'fixed';
457+
textarea.style.left = '-9999px';
458+
textarea.style.top = '0';
459+
textarea.setAttribute('readonly', '');
460+
document.body.appendChild(textarea);
461+
textarea.select();
462+
try {
463+
var success = document.execCommand('copy');
464+
document.body.removeChild(textarea);
465+
if (success) { resolve(); }
466+
else { reject(new Error('execCommand copy failed')); }
467+
} catch (err) {
468+
document.body.removeChild(textarea);
469+
reject(err);
470+
}
471+
});
472+
}
446473
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
447474
const timestamp = el.getAttribute('data-timestamp');
448475
const date = new Date(timestamp);
@@ -487,7 +514,7 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
487514
copyBtn.addEventListener('click', function(e) {
488515
e.stopPropagation();
489516
const textToCopy = el.textContent.replace(/^Copy$/, '').trim();
490-
navigator.clipboard.writeText(textToCopy).then(function() {
517+
copyToClipboard(textToCopy).then(function() {
491518
copyBtn.textContent = 'Copied!';
492519
copyBtn.classList.add('copied');
493520
setTimeout(function() {
@@ -496,6 +523,8 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
496523
}, 2000);
497524
}).catch(function(err) {
498525
console.error('Failed to copy:', err);
526+
copyBtn.textContent = 'Failed';
527+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
499528
});
500529
});
501530
el.appendChild(copyBtn);
@@ -514,7 +543,7 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
514543
const content = cell.querySelector('.cell-content');
515544
textToCopy = content.textContent.trim();
516545
}
517-
navigator.clipboard.writeText(textToCopy).then(function() {
546+
copyToClipboard(textToCopy).then(function() {
518547
btn.textContent = 'Copied!';
519548
btn.classList.add('copied');
520549
setTimeout(function() {
@@ -523,6 +552,8 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
523552
}, 2000);
524553
}).catch(function(err) {
525554
console.error('Failed to copy cell:', err);
555+
btn.textContent = 'Failed';
556+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
526557
});
527558
});
528559
// Keyboard accessibility

tests/__snapshots__/test_generate_html/TestParseSessionFile.test_jsonl_generates_html.html

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,33 @@ <h1>Claude Code transcript</h1>
655655
</script>
656656
</div>
657657
<script>
658+
// Clipboard helper with fallback for older browsers
659+
function copyToClipboard(text) {
660+
// Modern browsers: use Clipboard API
661+
if (navigator.clipboard && navigator.clipboard.writeText) {
662+
return navigator.clipboard.writeText(text);
663+
}
664+
// Fallback: use execCommand('copy')
665+
return new Promise(function(resolve, reject) {
666+
var textarea = document.createElement('textarea');
667+
textarea.value = text;
668+
textarea.style.position = 'fixed';
669+
textarea.style.left = '-9999px';
670+
textarea.style.top = '0';
671+
textarea.setAttribute('readonly', '');
672+
document.body.appendChild(textarea);
673+
textarea.select();
674+
try {
675+
var success = document.execCommand('copy');
676+
document.body.removeChild(textarea);
677+
if (success) { resolve(); }
678+
else { reject(new Error('execCommand copy failed')); }
679+
} catch (err) {
680+
document.body.removeChild(textarea);
681+
reject(err);
682+
}
683+
});
684+
}
658685
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
659686
const timestamp = el.getAttribute('data-timestamp');
660687
const date = new Date(timestamp);
@@ -699,7 +726,7 @@ <h1>Claude Code transcript</h1>
699726
copyBtn.addEventListener('click', function(e) {
700727
e.stopPropagation();
701728
const textToCopy = el.textContent.replace(/^Copy$/, '').trim();
702-
navigator.clipboard.writeText(textToCopy).then(function() {
729+
copyToClipboard(textToCopy).then(function() {
703730
copyBtn.textContent = 'Copied!';
704731
copyBtn.classList.add('copied');
705732
setTimeout(function() {
@@ -708,6 +735,8 @@ <h1>Claude Code transcript</h1>
708735
}, 2000);
709736
}).catch(function(err) {
710737
console.error('Failed to copy:', err);
738+
copyBtn.textContent = 'Failed';
739+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
711740
});
712741
});
713742
el.appendChild(copyBtn);
@@ -726,7 +755,7 @@ <h1>Claude Code transcript</h1>
726755
const content = cell.querySelector('.cell-content');
727756
textToCopy = content.textContent.trim();
728757
}
729-
navigator.clipboard.writeText(textToCopy).then(function() {
758+
copyToClipboard(textToCopy).then(function() {
730759
btn.textContent = 'Copied!';
731760
btn.classList.add('copied');
732761
setTimeout(function() {
@@ -735,6 +764,8 @@ <h1>Claude Code transcript</h1>
735764
}, 2000);
736765
}).catch(function(err) {
737766
console.error('Failed to copy cell:', err);
767+
btn.textContent = 'Failed';
768+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
738769
});
739770
});
740771
// Keyboard accessibility

0 commit comments

Comments
 (0)