Skip to content

Commit ab65c8d

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 26acdcd commit ab65c8d

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
@@ -305,6 +305,7 @@ def set_github_repo(repo: str | None) -> contextvars.Token[str | None]:
305305
_github_repo = repo
306306
return _github_repo_var.set(repo)
307307

308+
308309
# API constants
309310
API_BASE_URL = "https://api.anthropic.com/v1"
310311
ANTHROPIC_VERSION = "2023-06-01"
@@ -1800,6 +1801,33 @@ def render_message_with_tool_pairs(
18001801
"""
18011802

18021803
JS = """
1804+
// Clipboard helper with fallback for older browsers
1805+
function copyToClipboard(text) {
1806+
// Modern browsers: use Clipboard API
1807+
if (navigator.clipboard && navigator.clipboard.writeText) {
1808+
return navigator.clipboard.writeText(text);
1809+
}
1810+
// Fallback: use execCommand('copy')
1811+
return new Promise(function(resolve, reject) {
1812+
var textarea = document.createElement('textarea');
1813+
textarea.value = text;
1814+
textarea.style.position = 'fixed';
1815+
textarea.style.left = '-9999px';
1816+
textarea.style.top = '0';
1817+
textarea.setAttribute('readonly', '');
1818+
document.body.appendChild(textarea);
1819+
textarea.select();
1820+
try {
1821+
var success = document.execCommand('copy');
1822+
document.body.removeChild(textarea);
1823+
if (success) { resolve(); }
1824+
else { reject(new Error('execCommand copy failed')); }
1825+
} catch (err) {
1826+
document.body.removeChild(textarea);
1827+
reject(err);
1828+
}
1829+
});
1830+
}
18031831
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
18041832
const timestamp = el.getAttribute('data-timestamp');
18051833
const date = new Date(timestamp);
@@ -1844,7 +1872,7 @@ def render_message_with_tool_pairs(
18441872
copyBtn.addEventListener('click', function(e) {
18451873
e.stopPropagation();
18461874
const textToCopy = el.textContent.replace(/^Copy$/, '').trim();
1847-
navigator.clipboard.writeText(textToCopy).then(function() {
1875+
copyToClipboard(textToCopy).then(function() {
18481876
copyBtn.textContent = 'Copied!';
18491877
copyBtn.classList.add('copied');
18501878
setTimeout(function() {
@@ -1853,6 +1881,8 @@ def render_message_with_tool_pairs(
18531881
}, 2000);
18541882
}).catch(function(err) {
18551883
console.error('Failed to copy:', err);
1884+
copyBtn.textContent = 'Failed';
1885+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
18561886
});
18571887
});
18581888
el.appendChild(copyBtn);
@@ -1871,7 +1901,7 @@ def render_message_with_tool_pairs(
18711901
const content = cell.querySelector('.cell-content');
18721902
textToCopy = content.textContent.trim();
18731903
}
1874-
navigator.clipboard.writeText(textToCopy).then(function() {
1904+
copyToClipboard(textToCopy).then(function() {
18751905
btn.textContent = 'Copied!';
18761906
btn.classList.add('copied');
18771907
setTimeout(function() {
@@ -1880,6 +1910,8 @@ def render_message_with_tool_pairs(
18801910
}, 2000);
18811911
}).catch(function(err) {
18821912
console.error('Failed to copy cell:', err);
1913+
btn.textContent = 'Failed';
1914+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
18831915
});
18841916
});
18851917
// 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
@@ -663,6 +663,33 @@ <h1>Claude Code transcript</h1>
663663
</script>
664664
</div>
665665
<script>
666+
// Clipboard helper with fallback for older browsers
667+
function copyToClipboard(text) {
668+
// Modern browsers: use Clipboard API
669+
if (navigator.clipboard && navigator.clipboard.writeText) {
670+
return navigator.clipboard.writeText(text);
671+
}
672+
// Fallback: use execCommand('copy')
673+
return new Promise(function(resolve, reject) {
674+
var textarea = document.createElement('textarea');
675+
textarea.value = text;
676+
textarea.style.position = 'fixed';
677+
textarea.style.left = '-9999px';
678+
textarea.style.top = '0';
679+
textarea.setAttribute('readonly', '');
680+
document.body.appendChild(textarea);
681+
textarea.select();
682+
try {
683+
var success = document.execCommand('copy');
684+
document.body.removeChild(textarea);
685+
if (success) { resolve(); }
686+
else { reject(new Error('execCommand copy failed')); }
687+
} catch (err) {
688+
document.body.removeChild(textarea);
689+
reject(err);
690+
}
691+
});
692+
}
666693
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
667694
const timestamp = el.getAttribute('data-timestamp');
668695
const date = new Date(timestamp);
@@ -707,7 +734,7 @@ <h1>Claude Code transcript</h1>
707734
copyBtn.addEventListener('click', function(e) {
708735
e.stopPropagation();
709736
const textToCopy = el.textContent.replace(/^Copy$/, '').trim();
710-
navigator.clipboard.writeText(textToCopy).then(function() {
737+
copyToClipboard(textToCopy).then(function() {
711738
copyBtn.textContent = 'Copied!';
712739
copyBtn.classList.add('copied');
713740
setTimeout(function() {
@@ -716,6 +743,8 @@ <h1>Claude Code transcript</h1>
716743
}, 2000);
717744
}).catch(function(err) {
718745
console.error('Failed to copy:', err);
746+
copyBtn.textContent = 'Failed';
747+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
719748
});
720749
});
721750
el.appendChild(copyBtn);
@@ -734,7 +763,7 @@ <h1>Claude Code transcript</h1>
734763
const content = cell.querySelector('.cell-content');
735764
textToCopy = content.textContent.trim();
736765
}
737-
navigator.clipboard.writeText(textToCopy).then(function() {
766+
copyToClipboard(textToCopy).then(function() {
738767
btn.textContent = 'Copied!';
739768
btn.classList.add('copied');
740769
setTimeout(function() {
@@ -743,6 +772,8 @@ <h1>Claude Code transcript</h1>
743772
}, 2000);
744773
}).catch(function(err) {
745774
console.error('Failed to copy cell:', err);
775+
btn.textContent = 'Failed';
776+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
746777
});
747778
});
748779
// 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
@@ -654,6 +654,33 @@ <h1>Claude Code transcript</h1>
654654
</script>
655655
</div>
656656
<script>
657+
// Clipboard helper with fallback for older browsers
658+
function copyToClipboard(text) {
659+
// Modern browsers: use Clipboard API
660+
if (navigator.clipboard && navigator.clipboard.writeText) {
661+
return navigator.clipboard.writeText(text);
662+
}
663+
// Fallback: use execCommand('copy')
664+
return new Promise(function(resolve, reject) {
665+
var textarea = document.createElement('textarea');
666+
textarea.value = text;
667+
textarea.style.position = 'fixed';
668+
textarea.style.left = '-9999px';
669+
textarea.style.top = '0';
670+
textarea.setAttribute('readonly', '');
671+
document.body.appendChild(textarea);
672+
textarea.select();
673+
try {
674+
var success = document.execCommand('copy');
675+
document.body.removeChild(textarea);
676+
if (success) { resolve(); }
677+
else { reject(new Error('execCommand copy failed')); }
678+
} catch (err) {
679+
document.body.removeChild(textarea);
680+
reject(err);
681+
}
682+
});
683+
}
657684
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
658685
const timestamp = el.getAttribute('data-timestamp');
659686
const date = new Date(timestamp);
@@ -698,7 +725,7 @@ <h1>Claude Code transcript</h1>
698725
copyBtn.addEventListener('click', function(e) {
699726
e.stopPropagation();
700727
const textToCopy = el.textContent.replace(/^Copy$/, '').trim();
701-
navigator.clipboard.writeText(textToCopy).then(function() {
728+
copyToClipboard(textToCopy).then(function() {
702729
copyBtn.textContent = 'Copied!';
703730
copyBtn.classList.add('copied');
704731
setTimeout(function() {
@@ -707,6 +734,8 @@ <h1>Claude Code transcript</h1>
707734
}, 2000);
708735
}).catch(function(err) {
709736
console.error('Failed to copy:', err);
737+
copyBtn.textContent = 'Failed';
738+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
710739
});
711740
});
712741
el.appendChild(copyBtn);
@@ -725,7 +754,7 @@ <h1>Claude Code transcript</h1>
725754
const content = cell.querySelector('.cell-content');
726755
textToCopy = content.textContent.trim();
727756
}
728-
navigator.clipboard.writeText(textToCopy).then(function() {
757+
copyToClipboard(textToCopy).then(function() {
729758
btn.textContent = 'Copied!';
730759
btn.classList.add('copied');
731760
setTimeout(function() {
@@ -734,6 +763,8 @@ <h1>Claude Code transcript</h1>
734763
}, 2000);
735764
}).catch(function(err) {
736765
console.error('Failed to copy cell:', err);
766+
btn.textContent = 'Failed';
767+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
737768
});
738769
});
739770
// Keyboard accessibility

0 commit comments

Comments
 (0)