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