A vibe coded tangled fork which supports pijul.
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 }}