1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2025-10-05 19:30:58 +00:00

chore(ui): replace repo-code jQuery use, convert file to typescript (#9337)

This PR moves repo-code.js to repo-code.ts (with appropriate changes for the JS -> TS conversion), adds e2e tests for file folding and file line permalink copying to fully cover the features implemented in repo-code, then removes the jQuery usage in the file in favor of vanilla JS.

* chore(ui): replace jQuery uses in repo-code.ts

* chore(ui): add copy line permalink test

* chore(ui): add file folding test

* chore(ui): convert repo-code to ts

  This commit additionally removes the use of `document.selection` for IE8
support, as we no longer offer support for the browser.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9337
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Jordan Atwood <nightfirecat@nightfirec.at>
Co-committed-by: Jordan Atwood <nightfirecat@nightfirec.at>
This commit is contained in:
Jordan Atwood 2025-09-21 08:08:47 +02:00 committed by 0ko
parent c08bdaacdb
commit 01419d9c36
4 changed files with 84 additions and 45 deletions

View file

@ -127,3 +127,30 @@ test('Unicode escape highlight', async ({page}) => {
await expect(page.locator('.tippy-box .view_git_blame[href$="/a-file#L1"]')).toBeVisible(); await expect(page.locator('.tippy-box .view_git_blame[href$="/a-file#L1"]')).toBeVisible();
await expect(page.locator('.tippy-box .copy-line-permalink[data-url$="/a-file#L1"]')).toBeVisible(); await expect(page.locator('.tippy-box .copy-line-permalink[data-url$="/a-file#L1"]')).toBeVisible();
}); });
test('File folding', async ({page}) => {
const filePath = '/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d';
const response = await page.goto(filePath);
expect(response?.status()).toBe(200);
const foldFileButton = page.locator('.fold-file');
const diffFileBody = page.locator('.diff-file-body');
await foldFileButton.click();
await expect(diffFileBody).toBeHidden();
await foldFileButton.click();
await expect(diffFileBody).toBeVisible();
});
test('Copy line permalink', async ({page}, workerInfo) => {
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Apple clipboard API addon - starting at just $499!');
const response = await page.goto('/user2/repo1/src/branch/master/README.md?display=source#L1');
expect(response?.status()).toBe(200);
await page.locator('.code-line-button').click();
// eslint-disable-next-line playwright/no-force-option
await page.locator('.tippy-box .copy-line-permalink').click({force: true});
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md?display=source#L1');
});

View file

@ -1,4 +1,5 @@
import {singleAnchorRegex, rangeAnchorRegex} from './repo-code.js'; import {singleAnchorRegex, rangeAnchorRegex} from './repo-code.ts';
import {test, expect} from '@playwright/test';
test('singleAnchorRegex', () => { test('singleAnchorRegex', () => {
expect(singleAnchorRegex.test('#L0')).toEqual(false); expect(singleAnchorRegex.test('#L0')).toEqual(false);

View file

@ -1,4 +1,3 @@
import $ from 'jquery';
import {svg} from '../svg.js'; import {svg} from '../svg.js';
import {invertFileFolding} from './file-fold.js'; import {invertFileFolding} from './file-fold.js';
import {createTippy} from '../modules/tippy.js'; import {createTippy} from '../modules/tippy.js';
@ -20,12 +19,12 @@ function isBlame() {
return Boolean(document.querySelector('div.blame')); return Boolean(document.querySelector('div.blame'));
} }
function getLineEls() { function getLineEls(): Element[] {
return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`); return Array.from(document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`));
} }
function selectRange($linesEls, $selectionEndEl, $selectionStartEls) { function selectRange(linesEls: Element[], selectionEndEl: Element, selectionStartEl?: Element) {
for (const el of $linesEls) { for (const el of linesEls) {
el.closest('tr').classList.remove('active'); el.closest('tr').classList.remove('active');
} }
@ -59,9 +58,9 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
copyPermalink.setAttribute('data-url', link); copyPermalink.setAttribute('data-url', link);
}; };
if ($selectionStartEls) { if (selectionStartEl) {
let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1)); let a = parseInt(selectionEndEl.getAttribute('rel').slice(1));
let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1)); let b = parseInt(selectionStartEl.getAttribute('rel').slice(1));
let c; let c;
if (a !== b) { if (a !== b) {
if (a > b) { if (a > b) {
@ -73,9 +72,9 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
for (let i = a; i <= b; i++) { for (let i = a; i <= b; i++) {
classes.push(`[rel=L${i}]`); classes.push(`[rel=L${i}]`);
} }
$linesEls.filter(classes.join(',')).each(function () { for (const selectedLine of linesEls.filter((line) => line.matches(classes.join(',')))) {
this.closest('tr').classList.add('active'); selectedLine.closest('tr').classList.add('active');
}); }
changeHash(`#L${a}-L${b}`); changeHash(`#L${a}-L${b}`);
updateIssueHref(`L${a}-L${b}`); updateIssueHref(`L${a}-L${b}`);
@ -84,12 +83,12 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
return; return;
} }
} }
$selectionEndEl[0].closest('tr').classList.add('active'); selectionEndEl.closest('tr').classList.add('active');
changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`); changeHash(`#${selectionEndEl.getAttribute('rel')}`);
updateIssueHref($selectionEndEl[0].getAttribute('rel')); updateIssueHref(selectionEndEl.getAttribute('rel'));
updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel')); updateViewGitBlameFragment(selectionEndEl.getAttribute('rel'));
updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel')); updateCopyPermalinkUrl(selectionEndEl.getAttribute('rel'));
} }
function showLineButton() { function showLineButton() {
@ -127,70 +126,82 @@ function showLineButton() {
} }
export function initRepoCodeView() { export function initRepoCodeView() {
if ($('.code-view .lines-num').length > 0) { if (document.querySelector('.code-view .lines-num')) {
$(document).on('click', '.lines-num span', function (e) { document.addEventListener('click', (e) => {
const target = e.target as Element;
if (!target.matches('.lines-num span')) {
return;
}
const linesEls = getLineEls(); const linesEls = getLineEls();
const selectedEls = Array.from(linesEls).filter((el) => { const selectedEl = linesEls.find((el) => {
return el.matches(`[rel=${this.getAttribute('id')}]`); return el.matches(`[rel=${target.id}]`);
}); });
let from; let from;
if (e.shiftKey) { if (e.shiftKey) {
from = Array.from(linesEls).filter((el) => { from = linesEls.find((el) => {
return el.closest('tr').classList.contains('active'); return el.closest('tr').classList.contains('active');
}); });
} }
selectRange($(linesEls), $(selectedEls), from ? $(from) : null); selectRange(linesEls, selectedEl, from);
if (window.getSelection) { window.getSelection().removeAllRanges();
window.getSelection().removeAllRanges();
} else {
document.selection.empty();
}
showLineButton(); showLineButton();
}); });
$(window).on('hashchange', () => { window.addEventListener('hashchange', () => {
let m = window.location.hash.match(rangeAnchorRegex); let m = window.location.hash.match(rangeAnchorRegex);
const $linesEls = $(getLineEls()); const linesEls = getLineEls();
let $first; let first;
if (m) { if (m) {
$first = $linesEls.filter(`[rel=L${m[1]}]`); first = linesEls.find((el) => el.matches(`[rel=L${m[1]}]`));
if ($first.length) { if (first) {
const $last = $linesEls.filter(`[rel=L${m[2]}]`); const last = linesEls.findLast((el) => el.matches(`[rel=L${m[2]}]`));
selectRange($linesEls, $first, $last.length ? $last : $linesEls.last()); selectRange(linesEls, first, last ?? linesEls.at(-1));
// show code view menu marker (don't show in blame page) // show code view menu marker (don't show in blame page)
if (!isBlame()) { if (!isBlame()) {
showLineButton(); showLineButton();
} }
$('html, body').scrollTop($first.offset().top - 200); window.scrollBy({top: first.getBoundingClientRect().top - 200});
return; return;
} }
} }
m = window.location.hash.match(singleAnchorRegex); m = window.location.hash.match(singleAnchorRegex);
if (m) { if (m) {
$first = $linesEls.filter(`[rel=L${m[1]}]`); first = linesEls.find((el) => el.matches(`[rel=L${m[1]}]`));
if ($first.length) { if (first) {
selectRange($linesEls, $first); selectRange(linesEls, first);
// show code view menu marker (don't show in blame page) // show code view menu marker (don't show in blame page)
if (!isBlame()) { if (!isBlame()) {
showLineButton(); showLineButton();
} }
$('html, body').scrollTop($first.offset().top - 200); window.scrollBy({top: first.getBoundingClientRect().top - 200});
} }
} }
}); });
window.dispatchEvent(new Event('hashchange')); window.dispatchEvent(new Event('hashchange'));
} }
$(document).on('click', '.fold-file', ({currentTarget}) => { document.addEventListener('click', (e) => {
invertFileFolding(currentTarget.closest('.file-content'), currentTarget); const target = e.target as Element;
const foldFileButton = target.closest('.fold-file');
if (!foldFileButton) {
return;
}
invertFileFolding(foldFileButton.closest('.file-content'), foldFileButton);
}); });
$(document).on('click', '.copy-line-permalink', async ({currentTarget}) => { document.addEventListener('click', async (e) => {
await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url'))); const target = e.target as Element;
if (!target.matches('.copy-line-permalink')) {
return;
}
await clippie(toAbsoluteUrl(target.getAttribute('data-url')));
}); });
} }

View file

@ -48,7 +48,7 @@ import {initRepoTopicBar} from './features/repo-home.js';
import {initAdminEmails} from './features/admin/emails.js'; import {initAdminEmails} from './features/admin/emails.js';
import {initAdminCommon} from './features/admin/common.js'; import {initAdminCommon} from './features/admin/common.js';
import {initRepoTemplateSearch} from './features/repo-template.js'; import {initRepoTemplateSearch} from './features/repo-template.js';
import {initRepoCodeView} from './features/repo-code.js'; import {initRepoCodeView} from './features/repo-code.ts';
import {initSshKeyFormParser} from './features/sshkey-helper.js'; import {initSshKeyFormParser} from './features/sshkey-helper.js';
import {initRepoArchiveLinks} from './features/repo-common.js'; import {initRepoArchiveLinks} from './features/repo-common.js';
import {initRepoMigrationStatusChecker} from './features/repo-migrate.js'; import {initRepoMigrationStatusChecker} from './features/repo-migrate.js';