Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

keyboard-ux.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

keyboard-ux.tsBlame129 lines · 1 contributor
80bd7c8Claude1/**
2 * Keyboard UX enhancements for comment/form pages.
3 *
4 * Two self-contained IIFE scripts injected via
5 * `<script dangerouslySetInnerHTML={{ __html: ... }} />`:
6 *
7 * - `ctrlEnterSubmitScript()` — Ctrl+Enter / Cmd+Enter submits the
8 * closest form from any focused <textarea>.
9 * - `codeBlockCopyScript()` — adds a "Copy" button to every
10 * `<pre><code>` block inside rendered markdown containers.
11 */
12
13/**
14 * Returns an IIFE string that intercepts Ctrl+Enter (and Cmd+Enter on Mac)
15 * on any <textarea> and submits its parent <form>.
16 *
17 * If the form has a primary submit button it is clicked (triggering any
18 * formaction / validation logic); otherwise `form.submit()` is called as
19 * a fallback.
20 */
21export function ctrlEnterSubmitScript(): string {
22 return `(function(){
23 try{
24 document.addEventListener('keydown',function(e){
25 if(!(e.ctrlKey||e.metaKey))return;
26 if(e.key!=='Enter')return;
27 var ta=e.target;
28 if(!ta||ta.tagName!=='TEXTAREA')return;
29 e.preventDefault();
30 var form=ta.closest('form');
31 if(!form)return;
32 var submitBtn=form.querySelector('button[type="submit"],input[type="submit"]');
33 if(submitBtn){submitBtn.click();}else{form.submit();}
34 });
35 }catch(ex){}
36})();`;
37}
38
39/**
40 * Returns an IIFE string that adds a floating "Copy" button to every
41 * `<pre>` inside rendered markdown containers (.markdown-content,
42 * .prs-comment-body, .prs-body, .issue-body, .iss-body, .markdown-body).
43 *
44 * The button is injected on DOMContentLoaded and re-checked whenever new
45 * nodes are added via MutationObserver (for dynamically loaded content).
46 *
47 * Scoped CSS is injected once into <head>; a data attribute guards against
48 * double-injection on the same <pre>.
49 */
50export function codeBlockCopyScript(): string {
51 return `(function(){
52 try{
53 var _ccCss='.code-copy-btn{position:absolute;top:6px;right:6px;padding:3px 8px;font-size:11px;font-weight:600;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.08);color:rgba(255,255,255,0.70);cursor:pointer;opacity:0;transition:opacity 120ms;}.pre:hover .code-copy-btn,.code-copy-btn:focus{opacity:1;}pre:hover .code-copy-btn,.code-copy-btn:focus{opacity:1;}.code-copy-btn.copied{color:#34d399;border-color:rgba(52,211,153,0.40);}.code-copy-wrap{position:relative;}';
54 function injectCss(){if(document.getElementById('cc-style'))return;var s=document.createElement('style');s.id='cc-style';s.textContent=_ccCss;document.head.appendChild(s);}
55
56 function addCopyButtons(){
57 injectCss();
58 var pres=document.querySelectorAll('.markdown-content pre,.prs-comment-body pre,.prs-body pre,.issue-body pre,.iss-body pre,.markdown-body pre');
59 for(var i=0;i<pres.length;i++){
60 var pre=pres[i];
61 if(pre.getAttribute('data-copy-wired'))continue;
62 pre.setAttribute('data-copy-wired','1');
63
64 /* Wrap in a position:relative container if not already wrapped */
65 var parent=pre.parentElement;
66 var wrap;
67 if(parent&&parent.classList&&parent.classList.contains('code-copy-wrap')){
68 wrap=parent;
69 }else{
70 wrap=document.createElement('div');
71 wrap.className='code-copy-wrap';
72 parent.insertBefore(wrap,pre);
73 wrap.appendChild(pre);
74 }
75
76 /* Build the copy button */
77 var btn=document.createElement('button');
78 btn.type='button';
79 btn.className='code-copy-btn';
80 btn.textContent='Copy';
81 btn.setAttribute('aria-label','Copy code to clipboard');
82 (function(b,p){
83 b.addEventListener('click',function(){
84 var text=p.textContent||'';
85 if(!navigator.clipboard){
86 /* Fallback for non-HTTPS or older browsers */
87 try{
88 var ta=document.createElement('textarea');
89 ta.value=text;ta.style.position='fixed';ta.style.top='-9999px';
90 document.body.appendChild(ta);ta.select();
91 document.execCommand('copy');
92 document.body.removeChild(ta);
93 }catch(er){return;}
94 b.classList.add('copied');b.textContent='Copied!';
95 setTimeout(function(){b.classList.remove('copied');b.textContent='Copy';},1500);
96 return;
97 }
98 navigator.clipboard.writeText(text).then(function(){
99 b.classList.add('copied');b.textContent='Copied!';
100 setTimeout(function(){b.classList.remove('copied');b.textContent='Copy';},1500);
101 }).catch(function(){});
102 });
103 })(btn,pre);
104
105 wrap.appendChild(btn);
106 }
107 }
108
109 if(document.readyState==='loading'){
110 document.addEventListener('DOMContentLoaded',addCopyButtons);
111 }else{
112 addCopyButtons();
113 }
114
115 /* Watch for dynamically inserted markdown containers */
116 if(typeof MutationObserver!=='undefined'){
117 var obs=new MutationObserver(function(mutations){
118 for(var m=0;m<mutations.length;m++){
119 if(mutations[m].addedNodes&&mutations[m].addedNodes.length){
120 addCopyButtons();
121 break;
122 }
123 }
124 });
125 obs.observe(document.body,{childList:true,subtree:true});
126 }
127 }catch(ex){}
128})();`;
129}