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  }