|
1157 | 1157 | } else { |
1158 | 1158 | const {l,oN,nN} = seg; |
1159 | 1159 | const sign = l.type==='add'?'+':l.type==='del'?'-':' '; |
1160 | | - const content = JSON.stringify(l.content); |
1161 | | - h += `<div class="dl ${l.type}"> |
| 1160 | + // Use data-copy attribute — never put user content inside onclick |
| 1161 | + h += `<div class="dl ${l.type}" data-copy="${EA(l.content)}"> |
1162 | 1162 | <div class="ln">${oN}</div> |
1163 | 1163 | <div class="ln">${nN}</div> |
1164 | 1164 | <div class="lc"><span class="sign">${sign}</span>${EH(l.content)}</div> |
1165 | | - <button class="clb" onclick="copyText(${content})" title="Copy line">⎘</button> |
| 1165 | + <button class="clb" title="Copy line">⎘</button> |
1166 | 1166 | </div>`; |
1167 | 1167 | } |
1168 | 1168 | } |
|
1236 | 1236 | const inner = _id(id+'-inner'); |
1237 | 1237 | if (!inner) return; |
1238 | 1238 | const lines = []; |
1239 | | - inner.querySelectorAll('.dl').forEach(row => { |
1240 | | - const lc = row.querySelector('.lc'); |
1241 | | - if (!lc) return; |
1242 | | - const sign = lc.querySelector('.sign')?.textContent || ''; |
1243 | | - const text = lc.textContent.replace(sign, ''); |
| 1239 | + inner.querySelectorAll('.dl.add[data-copy], .dl.del[data-copy]').forEach(row => { |
| 1240 | + const text = row.dataset.copy || ''; |
1244 | 1241 | if (row.classList.contains('add')) lines.push('+' + text); |
1245 | 1242 | else if (row.classList.contains('del')) lines.push('-' + text); |
1246 | 1243 | }); |
|
1320 | 1317 | ]); |
1321 | 1318 | const lines = fc.content.split('\n'); |
1322 | 1319 | const isLarge = lines.length > 500; |
| 1320 | + // Store file content for copy-all (safe - not in HTML attribute) |
| 1321 | + window._fileCopyContent = fc.content; |
1323 | 1322 | let h = backBtn('showOverview()'); |
1324 | 1323 | h += `<div style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:9px 12px;margin-bottom:10px;display:flex;align-items:center;gap:8px"> |
1325 | 1324 | ${fileIcon(path)}<span style="font-size:13px;font-weight:500">${E(path)}</span> |
1326 | 1325 | <span style="margin-left:auto;display:flex;gap:8px;align-items:center"> |
1327 | 1326 | <span style="font-size:11px;color:var(--text2)">${lines.length} lines</span> |
1328 | 1327 | <button class="btn sm" onclick="showBlame('${ea(path)}')">Blame</button> |
1329 | | - <button class="btn sm" onclick="copyText(${JSON.stringify(fc.content)})">Copy all</button> |
| 1328 | + <button class="btn sm" onclick="copyText(window._fileCopyContent)">Copy all</button> |
1330 | 1329 | </span> |
1331 | 1330 | </div> |
1332 | 1331 | <div class="ctabs"> |
|
1346 | 1345 | h += renderSegs(fileDataMap[id].segs, 0, CHUNK) + sentinel(id); |
1347 | 1346 | } else { |
1348 | 1347 | lines.forEach((l,i) => { |
1349 | | - h += `<div class="dl ctx"><div class="ln" style="width:50px">${i+1}</div><div class="lc">${EH(l)}</div><button class="clb" onclick="copyText(${JSON.stringify(l)})" title="Copy line">⎘</button></div>`; |
| 1348 | + h += `<div class="dl ctx" data-copy="${EA(l)}"><div class="ln" style="width:50px">${i+1}</div><div class="lc">${EH(l)}</div><button class="clb" title="Copy line">⎘</button></div>`; |
1350 | 1349 | }); |
1351 | 1350 | } |
1352 | 1351 | h += `</div></div></div></div> |
|
1492 | 1491 | function E(s) { if(s==null)return'';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } |
1493 | 1492 | function EH(s) { if(s==null)return'';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } |
1494 | 1493 | function ea(s) { return String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'"); } |
| 1494 | +// EA: escape for HTML attribute value (double-quotes) — safe for data-* attributes |
| 1495 | +function EA(s) { if(s==null)return'';return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); } |
1495 | 1496 |
|
1496 | 1497 | function notify(msg, type='') { |
1497 | 1498 | const el = _id('notif'); |
|
1500 | 1501 | } |
1501 | 1502 |
|
1502 | 1503 | async function copyText(text) { |
1503 | | - try { await navigator.clipboard.writeText(text); } |
1504 | | - catch(e) { const ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta); } |
| 1504 | + try { await navigator.clipboard.writeText(String(text ?? '')); } |
| 1505 | + catch(e) { const ta=document.createElement('textarea');ta.value=String(text??'');ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta); } |
1505 | 1506 | notify('📋 Copied!','ok'); |
1506 | 1507 | } |
1507 | 1508 |
|
| 1509 | +// ================================================================ |
| 1510 | +// DELEGATED EVENT: copy-line buttons (.clb) |
| 1511 | +// Reads from data-copy on the parent .dl row — never uses inline onclick |
| 1512 | +// ================================================================ |
| 1513 | +document.addEventListener('click', e => { |
| 1514 | + const btn = e.target.closest('.clb'); |
| 1515 | + if (!btn) return; |
| 1516 | + e.stopPropagation(); |
| 1517 | + const row = btn.closest('.dl'); |
| 1518 | + if (row && row.dataset.copy !== undefined) { |
| 1519 | + copyText(row.dataset.copy); |
| 1520 | + } |
| 1521 | +}); |
| 1522 | + |
1508 | 1523 | const AV_COLORS=['#1e88e5','#43a047','#e53935','#8e24aa','#f4511e','#00897b','#d81b60','#3949ab','#c0ca33','#00acc1']; |
1509 | 1524 | function avatarColor(name) { if(!name)return'#555';let h=0;for(let i=0;i<name.length;i++)h=(h*31+name.charCodeAt(i))&0xffffffff;return AV_COLORS[Math.abs(h)%AV_COLORS.length]; } |
1510 | 1525 | function avatarInit(name) { if(!name)return'?';return name.trim().split(/\s+/).map(w=>w[0]).join('').slice(0,2).toUpperCase(); } |
|
0 commit comments