feat: remove fomantic's tab module (#8587)

In similar vein of forgejo/forgejo#7416

- Fomantic's tab module is responsible for showing the right content when a tab is clicked upon. Most notably the Write/Preview tabs on the comment editor.
- Remove it and replace the javascript with our own function that is able to provide everything Forgejo needs.
- Replace the CSS with our own bare minimum CSS.
- No functionality or visual is affected by this replacement.
- E2E test added.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8587
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
This commit is contained in:
Gusted 2025-07-21 22:33:17 +02:00 committed by Gusted
commit 8e4f50a909
12 changed files with 123 additions and 1093 deletions

View file

@ -0,0 +1,66 @@
// @watch start
// templates/repo/diff/**
// web_src/css/features/imagediff.css
// web_src/css/modules/tab.css
// web_src/js/modules/tab.ts
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test, dynamic_id} from './utils_e2e.ts';
test.use({user: 'user2'});
test('Repository image diff', async ({page}) => {
// Generate a temporary SVG and edit it.
let response = await page.goto('/user2/repo1/_new/master', {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const filename = `${dynamic_id()}.svg`;
await page.getByPlaceholder('Name your file…').fill(filename);
await page.locator('.monaco-editor').click();
await page.keyboard.type('<svg version="1.1" width="300" height="200" xmlns="http://www.w3.org/2000/svg"><circle cx="150" cy="100" r="80" fill="green" /></svg>\n');
await page.locator('.quick-pull-choice input[value="direct"]').click();
await page.getByRole('button', {name: 'Commit changes'}).click();
response = await page.goto(`/user2/repo1/_edit/master/${filename}`, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
await page.locator('.monaco-editor').click();
await page.keyboard.press('Meta+KeyA');
await page.keyboard.type('<svg version="1.1" width="300" height="200" xmlns="http://www.w3.org/2000/svg"><circle cx="150" cy="100" r="80" fill="red" /></svg>\n');
await page.locator('.quick-pull-choice input[value="direct"]').click();
await page.getByRole('button', {name: 'Commit changes'}).click();
// Go to the commit page, where a image diff is shown.
await page.locator('.commit-summary a.default-link').click();
// Exhaustively test tabs works as expected
await expect(page.locator('.item[data-tab="diff-side-by-side-1"]')).toContainClass('active');
await expect(page.locator('.item[data-tab="diff-swipe-1"]')).not.toContainClass('active');
await expect(page.locator('.item[data-tab="diff-overlay-1"]')).not.toContainClass('active');
await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeVisible();
await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeHidden();
await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeHidden();
await save_visual(page);
await page.getByText('Swipe').click();
await expect(page.locator('.item[data-tab="diff-side-by-side-1"]')).not.toContainClass('active');
await expect(page.locator('.item[data-tab="diff-swipe-1"]')).toContainClass('active');
await expect(page.locator('.item[data-tab="diff-overlay-1"]')).not.toContainClass('active');
await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeHidden();
await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeVisible();
await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeHidden();
await save_visual(page);
await page.getByText('Overlay').click();
await expect(page.locator('.item[data-tab="diff-side-by-side-1"]')).not.toContainClass('active');
await expect(page.locator('.item[data-tab="diff-swipe-1"]')).not.toContainClass('active');
await expect(page.locator('.item[data-tab="diff-overlay-1"]')).toContainClass('active');
await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeHidden();
await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeHidden();
await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeVisible();
await save_visual(page);
});

View file

@ -1,4 +1,6 @@
// @watch start // @watch start
// web_src/js/modules/tab.ts
// web_src/css/modules/tab.css
// web_src/js/features/comp/ComboMarkdownEditor.js // web_src/js/features/comp/ComboMarkdownEditor.js
// web_src/css/editor/combomarkdowneditor.css // web_src/css/editor/combomarkdowneditor.css
// templates/shared/combomarkdowneditor.tmpl // templates/shared/combomarkdowneditor.tmpl

View file

@ -12,6 +12,7 @@
@import "./modules/segment.css"; @import "./modules/segment.css";
@import "./modules/grid.css"; @import "./modules/grid.css";
@import "./modules/message.css"; @import "./modules/message.css";
@import "./modules/tab.css";
@import "./modules/table.css"; @import "./modules/table.css";
@import "./modules/card.css"; @import "./modules/card.css";
@import "./modules/checkbox.css"; @import "./modules/checkbox.css";

View file

@ -0,0 +1,7 @@
.ui.tab {
display: none;
}
.ui.tab.active {
display: block;
}

View file

@ -9277,86 +9277,4 @@ Floated Menu / Item
/******************************* /*******************************
Site Overrides Site Overrides
*******************************/
/*!
* # Fomantic-UI - Tab
* http://github.com/fomantic/Fomantic-UI/
*
*
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/
/*******************************
UI Tabs
*******************************/
.ui.tab {
display: none;
}
/*******************************
States
*******************************/
/*--------------------
Active
---------------------*/
.ui.tab.active,
.ui.tab.open {
display: block;
}
/*--------------------
Loading
---------------------*/
.ui.tab.loading {
position: relative;
overflow: hidden;
display: block;
min-height: 250px;
}
.ui.tab.loading * {
position: relative !important;
left: -10000px !important;
}
.ui.tab.loading:before,
.ui.tab.loading.segment:before {
position: absolute;
content: '';
top: 50%;
left: 50%;
margin: -1.25em 0 0 -1.25em;
width: 2.5em;
height: 2.5em;
border-radius: 500rem;
border: 0.2em solid rgba(0, 0, 0, 0.1);
}
.ui.tab.loading:after,
.ui.tab.loading.segment:after {
position: absolute;
content: '';
top: 50%;
left: 50%;
margin: -1.25em 0 0 -1.25em;
width: 2.5em;
height: 2.5em;
animation: loader 0.6s infinite linear;
border: 0.2em solid #767676;
border-radius: 500rem;
box-shadow: 0 0 0 1px transparent;
}
/*******************************
Tab Overrides
*******************************/
/*******************************
User Overrides
*******************************/ *******************************/

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,6 @@
"form", "form",
"menu", "menu",
"modal", "modal",
"search", "search"
"tab"
] ]
} }

View file

@ -12,6 +12,7 @@ import {confirmModal} from './comp/ConfirmModal.js';
import {showErrorToast} from '../modules/toast.js'; import {showErrorToast} from '../modules/toast.js';
import {request, POST, GET} from '../modules/fetch.js'; import {request, POST, GET} from '../modules/fetch.js';
import '../htmx.js'; import '../htmx.js';
import {initTab} from '../modules/tab.ts';
const {appUrl, appSubUrl, csrfToken, i18n} = window.config; const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
@ -195,7 +196,9 @@ export function initGlobalCommon() {
$uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward'); $uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward');
$uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward'); $uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward');
$('.tabular.menu .item').tab(); for (const el of document.querySelectorAll('.tabular.menu')) {
initTab(el);
}
initSubmitEventPolyfill(); initSubmitEventPolyfill();
document.addEventListener('submit', formFetchAction); document.addEventListener('submit', formFetchAction);

View file

@ -10,6 +10,7 @@ import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
import {initTextExpander} from './TextExpander.js'; import {initTextExpander} from './TextExpander.js';
import {showErrorToast, showHintToast} from '../../modules/toast.js'; import {showErrorToast, showHintToast} from '../../modules/toast.js';
import {POST} from '../../modules/fetch.js'; import {POST} from '../../modules/fetch.js';
import {initTab} from '../../modules/tab.ts';
/** /**
* validate if the given textarea is non-empty. * validate if the given textarea is non-empty.
@ -200,7 +201,8 @@ class ComboMarkdownEditor {
setupTab() { setupTab() {
const $container = $(this.container); const $container = $(this.container);
const tabs = $container[0].querySelectorAll('.switch > .item'); const switchEl = $container[0].querySelector('.switch');
const tabs = switchEl.querySelectorAll('.item');
// Fomantic Tab requires the "data-tab" to be globally unique. // Fomantic Tab requires the "data-tab" to be globally unique.
// So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic. // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
@ -221,7 +223,7 @@ class ComboMarkdownEditor {
}); });
}); });
$(tabs).tab(); initTab(switchEl);
this.previewUrl = tabPreviewer.getAttribute('data-preview-url'); this.previewUrl = tabPreviewer.getAttribute('data-preview-url');
this.previewContext = tabPreviewer.getAttribute('data-preview-context'); this.previewContext = tabPreviewer.getAttribute('data-preview-context');

View file

@ -5,10 +5,11 @@ import {hideElem, showElem, createElementFromHTML} from '../utils/dom.js';
import {initMarkupContent} from '../markup/content.js'; import {initMarkupContent} from '../markup/content.js';
import {attachRefIssueContextPopup} from './contextpopup.js'; import {attachRefIssueContextPopup} from './contextpopup.js';
import {POST} from '../modules/fetch.js'; import {POST} from '../modules/fetch.js';
import {initTab} from '../modules/tab.ts';
function initEditPreviewTab($form) { function initEditPreviewTab($form) {
const $tabMenu = $form.find('.tabular.menu'); const $tabMenu = $form.find('.tabular.menu');
$tabMenu.find('.item').tab(); initTab($tabMenu[0]);
const $previewTab = $tabMenu.find( const $previewTab = $tabMenu.find(
`.item[data-tab="${$tabMenu.data('preview')}"]`, `.item[data-tab="${$tabMenu.data('preview')}"]`,
); );

View file

@ -11,9 +11,6 @@ import {initDimmer} from './dimmer.ts';
export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)'); export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)');
export function initGiteaFomantic() { export function initGiteaFomantic() {
// Silence fomantic's error logging when tabs are used without a target content element
$.fn.tab.settings.silent = true;
// By default, use "exact match" for full text search // By default, use "exact match" for full text search
$.fn.dropdown.settings.fullTextSearch = 'exact'; $.fn.dropdown.settings.fullTextSearch = 'exact';
// Do not use "cursor: pointer" for dropdown labels // Do not use "cursor: pointer" for dropdown labels

36
web_src/js/modules/tab.ts Normal file
View file

@ -0,0 +1,36 @@
export function initTab(parentEl: Element) {
if (!parentEl) {
return;
}
// Keep track of which tab is active for this element.
let activeTabPath = parentEl.querySelector('.item.active')?.getAttribute('data-tab');
if (!activeTabPath) {
return;
}
for (const el of parentEl.querySelectorAll('.item')) {
el.addEventListener('click', (ev) => {
// There's no data-tab attribute we can't do anything, ignore.
const tabPath = el.getAttribute('data-tab');
if (!tabPath) {
return;
}
// The item is already active, ignore.
if (el.classList.contains('active')) {
return;
}
// Make the current item active and the previous item inactive.
parentEl.querySelector('.item.active').classList.remove('active');
document.querySelector(`.tab.active[data-tab=${activeTabPath}]`).classList.remove('active');
el.classList.add('active');
document.querySelector(`.tab[data-tab=${tabPath}]`).classList.add('active');
activeTabPath = tabPath;
// Not really sure if this is useful, it is kept from how Fomantic did it.
ev.preventDefault();
}, {passive: false});
}
}