A vibe coded tangled fork which supports pijul.
at 16f25ff5d55581ae0d83cbeac370393a081e2763 132 lines 4.5 kB view raw
1{{ define "fragments/line-quote-button" }} 2<button 3 id="line-quote-btn" 4 type="button" 5 aria-label="Quote line in comment" 6 class="hidden fixed z-50 p-0.5 rounded bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer shadow-sm transition-opacity opacity-0" 7 style="pointer-events: none;" 8> 9 {{ i "message-square-quote" "w-3.5 h-3.5" }} 10</button> 11<script> 12 (() => { 13 const btn = document.getElementById('line-quote-btn'); 14 if (!btn) return; 15 16 let currentAnchor = null; 17 let currentFileName = null; 18 19 const findTextarea = () => 20 document.getElementById('pull-comment-textarea') 21 || document.getElementById('comment-textarea'); 22 23 const findLineEl = (el) => 24 el?.closest?.('.line') 25 || el?.closest?.('span[id*="-O"]') 26 || el?.closest?.('span[id*="-N"]'); 27 28 const getAnchor = (lineEl) => { 29 const link = lineEl.querySelector('a[href^="#"]'); 30 return link ? link.getAttribute('href').slice(1) : lineEl.id || null; 31 }; 32 33 const getFileName = (lineEl) => { 34 const details = lineEl.closest('details[id^="file-"]'); 35 if (details) return details.id.replace(/^file-/, ''); 36 const bc = document.getElementById('breadcrumbs'); 37 if (!bc) return null; 38 const els = bc.querySelectorAll('.text-bold'); 39 return els.length > 0 ? els[els.length - 1].textContent.trim() : null; 40 }; 41 42 const show = (lineEl) => { 43 if (!findTextarea()) return; 44 const anchor = getAnchor(lineEl); 45 if (!anchor) return; 46 47 currentAnchor = anchor; 48 currentFileName = getFileName(lineEl); 49 50 const rect = lineEl.getBoundingClientRect(); 51 Object.assign(btn.style, { 52 top: `${rect.top + rect.height / 2 - btn.offsetHeight / 2}px`, 53 left: `${rect.left + 4}px`, 54 opacity: '1', 55 pointerEvents: 'auto', 56 }); 57 btn.classList.remove('hidden'); 58 }; 59 60 const hide = () => { 61 Object.assign(btn.style, { opacity: '0', pointerEvents: 'none' }); 62 setTimeout(() => { if (btn.style.opacity === '0') btn.classList.add('hidden'); }, 150); 63 }; 64 65 let hoverTarget = null; 66 67 document.addEventListener('mouseover', (e) => { 68 const lineEl = findLineEl(e.target); 69 if (lineEl && lineEl !== hoverTarget) { 70 hoverTarget = lineEl; 71 show(lineEl); 72 } 73 }); 74 75 document.addEventListener('mouseout', (e) => { 76 const lineEl = findLineEl(e.target); 77 if (!lineEl) return; 78 if (findLineEl(e.relatedTarget) === lineEl) return; 79 if (e.relatedTarget === btn || btn.contains(e.relatedTarget)) return; 80 hoverTarget = null; 81 hide(); 82 }); 83 84 btn.addEventListener('mouseleave', (e) => { 85 if (!findLineEl(e.relatedTarget)) { 86 hoverTarget = null; 87 hide(); 88 } 89 }); 90 91 btn.addEventListener('click', (e) => { 92 e.preventDefault(); 93 e.stopPropagation(); 94 const textarea = findTextarea(); 95 if (!textarea || !currentAnchor) return; 96 97 const lineNum = currentAnchor.match(/[ON]?(\d+)(?:-[ON]?\d+)?$/)?.[1]; 98 if (!lineNum) return; 99 100 const label = currentFileName ? `${currentFileName}:${lineNum}` : `L${lineNum}`; 101 const md = `[\`${label}\`](${window.location.pathname}#${currentAnchor})`; 102 103 const { selectionStart: start, selectionEnd: end, value } = textarea; 104 const before = value.slice(0, start); 105 const after = value.slice(end); 106 107 let prefix = ''; 108 let suffix = ''; 109 if (start === end && before.length > 0) { 110 const currentLine = before.slice(before.lastIndexOf('\n') + 1); 111 if (currentLine.length > 0) { 112 const nextNl = after.indexOf('\n'); 113 const restOfLine = nextNl === -1 ? after : after.slice(0, nextNl); 114 if (restOfLine.trim().length === 0) { 115 prefix = '\n'; 116 } else { 117 prefix = before.endsWith(' ') ? '' : ' '; 118 suffix = after.startsWith(' ') ? '' : ' '; 119 } 120 } 121 } 122 123 textarea.value = before + prefix + md + suffix + after; 124 const pos = start + prefix.length + md.length + suffix.length; 125 textarea.selectionStart = textarea.selectionEnd = pos; 126 textarea.focus(); 127 textarea.dispatchEvent(new Event('input', { bubbles: true })); 128 textarea.dispatchEvent(new Event('keyup', { bubbles: true })); 129 }); 130 })(); 131</script> 132{{ end }}