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.
| 80bd7c8 | 1 | /** |
| 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 | */ | |
| 21 | export 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 | */ | |
| 50 | export 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 | } |