code.gitea.io/gitea@v1.22.3/web_src/js/features/repo-code.js (about) 1 import $ from 'jquery'; 2 import {svg} from '../svg.js'; 3 import {invertFileFolding} from './file-fold.js'; 4 import {createTippy} from '../modules/tippy.js'; 5 import {clippie} from 'clippie'; 6 import {toAbsoluteUrl} from '../utils.js'; 7 8 export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/; 9 export const rangeAnchorRegex = /^#(L[1-9][0-9]*)-(L[1-9][0-9]*)$/; 10 11 function changeHash(hash) { 12 if (window.history.pushState) { 13 window.history.pushState(null, null, hash); 14 } else { 15 window.location.hash = hash; 16 } 17 } 18 19 function isBlame() { 20 return Boolean(document.querySelector('div.blame')); 21 } 22 23 function getLineEls() { 24 return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`); 25 } 26 27 function selectRange($linesEls, $selectionEndEl, $selectionStartEls) { 28 for (const el of $linesEls) { 29 el.closest('tr').classList.remove('active'); 30 } 31 32 // add hashchange to permalink 33 const refInNewIssue = document.querySelector('a.ref-in-new-issue'); 34 const copyPermalink = document.querySelector('a.copy-line-permalink'); 35 const viewGitBlame = document.querySelector('a.view_git_blame'); 36 37 const updateIssueHref = function (anchor) { 38 if (!refInNewIssue) return; 39 const urlIssueNew = refInNewIssue.getAttribute('data-url-issue-new'); 40 const urlParamBodyLink = refInNewIssue.getAttribute('data-url-param-body-link'); 41 const issueContent = `${toAbsoluteUrl(urlParamBodyLink)}#${anchor}`; // the default content for issue body 42 refInNewIssue.setAttribute('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`); 43 }; 44 45 const updateViewGitBlameFragment = function (anchor) { 46 if (!viewGitBlame) return; 47 let href = viewGitBlame.getAttribute('href'); 48 href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`; 49 if (anchor.length !== 0) { 50 href = `${href}#${anchor}`; 51 } 52 viewGitBlame.setAttribute('href', href); 53 }; 54 55 const updateCopyPermalinkUrl = function (anchor) { 56 if (!copyPermalink) return; 57 let link = copyPermalink.getAttribute('data-url'); 58 link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`; 59 copyPermalink.setAttribute('data-url', link); 60 }; 61 62 if ($selectionStartEls) { 63 let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1)); 64 let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1)); 65 let c; 66 if (a !== b) { 67 if (a > b) { 68 c = a; 69 a = b; 70 b = c; 71 } 72 const classes = []; 73 for (let i = a; i <= b; i++) { 74 classes.push(`[rel=L${i}]`); 75 } 76 $linesEls.filter(classes.join(',')).each(function () { 77 this.closest('tr').classList.add('active'); 78 }); 79 changeHash(`#L${a}-L${b}`); 80 81 updateIssueHref(`L${a}-L${b}`); 82 updateViewGitBlameFragment(`L${a}-L${b}`); 83 updateCopyPermalinkUrl(`L${a}-L${b}`); 84 return; 85 } 86 } 87 $selectionEndEl[0].closest('tr').classList.add('active'); 88 changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`); 89 90 updateIssueHref($selectionEndEl[0].getAttribute('rel')); 91 updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel')); 92 updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel')); 93 } 94 95 function showLineButton() { 96 const menu = document.querySelector('.code-line-menu'); 97 if (!menu) return; 98 99 // remove all other line buttons 100 for (const el of document.querySelectorAll('.code-line-button')) { 101 el.remove(); 102 } 103 104 // find active row and add button 105 const tr = document.querySelector('.code-view tr.active'); 106 const td = tr.querySelector('td.lines-num'); 107 const btn = document.createElement('button'); 108 btn.classList.add('code-line-button', 'ui', 'basic', 'button'); 109 btn.innerHTML = svg('octicon-kebab-horizontal'); 110 td.prepend(btn); 111 112 // put a copy of the menu back into DOM for the next click 113 btn.closest('.code-view').append(menu.cloneNode(true)); 114 115 createTippy(btn, { 116 theme: 'menu', 117 trigger: 'click', 118 hideOnClick: true, 119 content: menu, 120 placement: 'right-start', 121 interactive: true, 122 onShow: (tippy) => { 123 tippy.popper.addEventListener('click', () => { 124 tippy.hide(); 125 }, {once: true}); 126 }, 127 }); 128 } 129 130 export function initRepoCodeView() { 131 if ($('.code-view .lines-num').length > 0) { 132 $(document).on('click', '.lines-num span', function (e) { 133 const linesEls = getLineEls(); 134 const selectedEls = Array.from(linesEls).filter((el) => { 135 return el.matches(`[rel=${this.getAttribute('id')}]`); 136 }); 137 138 let from; 139 if (e.shiftKey) { 140 from = Array.from(linesEls).filter((el) => { 141 return el.closest('tr').classList.contains('active'); 142 }); 143 } 144 selectRange($(linesEls), $(selectedEls), from ? $(from) : null); 145 146 if (window.getSelection) { 147 window.getSelection().removeAllRanges(); 148 } else { 149 document.selection.empty(); 150 } 151 152 showLineButton(); 153 }); 154 155 $(window).on('hashchange', () => { 156 let m = window.location.hash.match(rangeAnchorRegex); 157 const $linesEls = $(getLineEls()); 158 let $first; 159 if (m) { 160 $first = $linesEls.filter(`[rel=${m[1]}]`); 161 if ($first.length) { 162 selectRange($linesEls, $first, $linesEls.filter(`[rel=${m[2]}]`)); 163 164 // show code view menu marker (don't show in blame page) 165 if (!isBlame()) { 166 showLineButton(); 167 } 168 169 $('html, body').scrollTop($first.offset().top - 200); 170 return; 171 } 172 } 173 m = window.location.hash.match(singleAnchorRegex); 174 if (m) { 175 $first = $linesEls.filter(`[rel=L${m[2]}]`); 176 if ($first.length) { 177 selectRange($linesEls, $first); 178 179 // show code view menu marker (don't show in blame page) 180 if (!isBlame()) { 181 showLineButton(); 182 } 183 184 $('html, body').scrollTop($first.offset().top - 200); 185 } 186 } 187 }).trigger('hashchange'); 188 } 189 $(document).on('click', '.fold-file', ({currentTarget}) => { 190 invertFileFolding(currentTarget.closest('.file-content'), currentTarget); 191 }); 192 $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => { 193 await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url'))); 194 }); 195 }