summaryrefslogtreecommitdiffstats
path: root/web_src/js/features/repo-code.js
diff options
context:
space:
mode:
Diffstat (limited to 'web_src/js/features/repo-code.js')
-rw-r--r--web_src/js/features/repo-code.js195
1 files changed, 195 insertions, 0 deletions
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
new file mode 100644
index 00000000..794cc380
--- /dev/null
+++ b/web_src/js/features/repo-code.js
@@ -0,0 +1,195 @@
+import $ from 'jquery';
+import {svg} from '../svg.js';
+import {invertFileFolding} from './file-fold.js';
+import {createTippy} from '../modules/tippy.js';
+import {clippie} from 'clippie';
+import {toAbsoluteUrl} from '../utils.js';
+
+export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/;
+export const rangeAnchorRegex = /^#(L[1-9][0-9]*)-(L[1-9][0-9]*)$/;
+
+function changeHash(hash) {
+ if (window.history.pushState) {
+ window.history.pushState(null, null, hash);
+ } else {
+ window.location.hash = hash;
+ }
+}
+
+function isBlame() {
+ return Boolean(document.querySelector('div.blame'));
+}
+
+function getLineEls() {
+ return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`);
+}
+
+function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
+ for (const el of $linesEls) {
+ el.closest('tr').classList.remove('active');
+ }
+
+ // add hashchange to permalink
+ const refInNewIssue = document.querySelector('a.ref-in-new-issue');
+ const copyPermalink = document.querySelector('a.copy-line-permalink');
+ const viewGitBlame = document.querySelector('a.view_git_blame');
+
+ const updateIssueHref = function (anchor) {
+ if (!refInNewIssue) return;
+ const urlIssueNew = refInNewIssue.getAttribute('data-url-issue-new');
+ const urlParamBodyLink = refInNewIssue.getAttribute('data-url-param-body-link');
+ const issueContent = `${toAbsoluteUrl(urlParamBodyLink)}#${anchor}`; // the default content for issue body
+ refInNewIssue.setAttribute('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`);
+ };
+
+ const updateViewGitBlameFragment = function (anchor) {
+ if (!viewGitBlame) return;
+ let href = viewGitBlame.getAttribute('href');
+ href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`;
+ if (anchor.length !== 0) {
+ href = `${href}#${anchor}`;
+ }
+ viewGitBlame.setAttribute('href', href);
+ };
+
+ const updateCopyPermalinkUrl = function (anchor) {
+ if (!copyPermalink) return;
+ let link = copyPermalink.getAttribute('data-url');
+ link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`;
+ copyPermalink.setAttribute('data-url', link);
+ };
+
+ if ($selectionStartEls) {
+ let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1));
+ let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1));
+ let c;
+ if (a !== b) {
+ if (a > b) {
+ c = a;
+ a = b;
+ b = c;
+ }
+ const classes = [];
+ for (let i = a; i <= b; i++) {
+ classes.push(`[rel=L${i}]`);
+ }
+ $linesEls.filter(classes.join(',')).each(function () {
+ this.closest('tr').classList.add('active');
+ });
+ changeHash(`#L${a}-L${b}`);
+
+ updateIssueHref(`L${a}-L${b}`);
+ updateViewGitBlameFragment(`L${a}-L${b}`);
+ updateCopyPermalinkUrl(`L${a}-L${b}`);
+ return;
+ }
+ }
+ $selectionEndEl[0].closest('tr').classList.add('active');
+ changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`);
+
+ updateIssueHref($selectionEndEl[0].getAttribute('rel'));
+ updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel'));
+ updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel'));
+}
+
+function showLineButton() {
+ const menu = document.querySelector('.code-line-menu');
+ if (!menu) return;
+
+ // remove all other line buttons
+ for (const el of document.querySelectorAll('.code-line-button')) {
+ el.remove();
+ }
+
+ // find active row and add button
+ const tr = document.querySelector('.code-view tr.active');
+ const td = tr.querySelector('td.lines-num');
+ const btn = document.createElement('button');
+ btn.classList.add('code-line-button', 'ui', 'basic', 'button');
+ btn.innerHTML = svg('octicon-kebab-horizontal');
+ td.prepend(btn);
+
+ // put a copy of the menu back into DOM for the next click
+ btn.closest('.code-view').append(menu.cloneNode(true));
+
+ createTippy(btn, {
+ trigger: 'click',
+ hideOnClick: true,
+ content: menu,
+ placement: 'right-start',
+ interactive: true,
+ onShow: (tippy) => {
+ tippy.popper.addEventListener('click', () => {
+ tippy.hide();
+ }, {once: true});
+ },
+ });
+}
+
+export function initRepoCodeView() {
+ if ($('.code-view .lines-num').length > 0) {
+ $(document).on('click', '.lines-num span', function (e) {
+ const linesEls = getLineEls();
+ const selectedEls = Array.from(linesEls).filter((el) => {
+ return el.matches(`[rel=${this.getAttribute('id')}]`);
+ });
+
+ let from;
+ if (e.shiftKey) {
+ from = Array.from(linesEls).filter((el) => {
+ return el.closest('tr').classList.contains('active');
+ });
+ }
+ selectRange($(linesEls), $(selectedEls), from ? $(from) : null);
+
+ if (window.getSelection) {
+ window.getSelection().removeAllRanges();
+ } else {
+ document.selection.empty();
+ }
+
+ showLineButton();
+ });
+
+ $(window).on('hashchange', () => {
+ let m = window.location.hash.match(rangeAnchorRegex);
+ const $linesEls = $(getLineEls());
+ let $first;
+ if (m) {
+ $first = $linesEls.filter(`[rel=${m[1]}]`);
+ if ($first.length) {
+ const $last = $linesEls.filter(`[rel=${m[2]}]`);
+ selectRange($linesEls, $first, $last.length ? $last : $linesEls.last());
+
+ // show code view menu marker (don't show in blame page)
+ if (!isBlame()) {
+ showLineButton();
+ }
+
+ $('html, body').scrollTop($first.offset().top - 200);
+ return;
+ }
+ }
+ m = window.location.hash.match(singleAnchorRegex);
+ if (m) {
+ $first = $linesEls.filter(`[rel=L${m[2]}]`);
+ if ($first.length) {
+ selectRange($linesEls, $first);
+
+ // show code view menu marker (don't show in blame page)
+ if (!isBlame()) {
+ showLineButton();
+ }
+
+ $('html, body').scrollTop($first.offset().top - 200);
+ }
+ }
+ }).trigger('hashchange');
+ }
+ $(document).on('click', '.fold-file', ({currentTarget}) => {
+ invertFileFolding(currentTarget.closest('.file-content'), currentTarget);
+ });
+ $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => {
+ await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url')));
+ });
+}