diff --git a/tests/e2e/image-diff.test.e2e.ts b/tests/e2e/image-diff.test.e2e.ts
new file mode 100644
index 0000000000..f7d4f7bd69
--- /dev/null
+++ b/tests/e2e/image-diff.test.e2e.ts
@@ -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('\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('\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);
+});
diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts
index 7983907343..d0ab88fe26 100644
--- a/tests/e2e/markdown-editor.test.e2e.ts
+++ b/tests/e2e/markdown-editor.test.e2e.ts
@@ -1,4 +1,6 @@
// @watch start
+// web_src/js/modules/tab.ts
+// web_src/css/modules/tab.css
// web_src/js/features/comp/ComboMarkdownEditor.js
// web_src/css/editor/combomarkdowneditor.css
// templates/shared/combomarkdowneditor.tmpl
diff --git a/web_src/css/index.css b/web_src/css/index.css
index e7e5dda2d5..d86a784bcc 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -12,6 +12,7 @@
@import "./modules/segment.css";
@import "./modules/grid.css";
@import "./modules/message.css";
+@import "./modules/tab.css";
@import "./modules/table.css";
@import "./modules/card.css";
@import "./modules/checkbox.css";
diff --git a/web_src/css/modules/tab.css b/web_src/css/modules/tab.css
new file mode 100644
index 0000000000..63c83179b2
--- /dev/null
+++ b/web_src/css/modules/tab.css
@@ -0,0 +1,7 @@
+.ui.tab {
+ display: none;
+}
+
+.ui.tab.active {
+ display: block;
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 6e9958faa8..bd1777db36 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -9277,86 +9277,4 @@ Floated Menu / Item
/*******************************
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
*******************************/
\ No newline at end of file
diff --git a/web_src/fomantic/build/semantic.js b/web_src/fomantic/build/semantic.js
index 393d9d8142..80ddb88eda 100644
--- a/web_src/fomantic/build/semantic.js
+++ b/web_src/fomantic/build/semantic.js
@@ -10234,1005 +10234,3 @@ $.fn.search.settings = {
};
})( jQuery, window, document );
-
-/*!
- * # Fomantic-UI - Tab
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-;(function ($, window, document, undefined) {
-
-'use strict';
-
-$.isWindow = $.isWindow || function(obj) {
- return obj != null && obj === obj.window;
-};
-$.isFunction = $.isFunction || function(obj) {
- return typeof obj === "function" && typeof obj.nodeType !== "number";
-};
-
-window = (typeof window != 'undefined' && window.Math == Math)
- ? window
- : (typeof self != 'undefined' && self.Math == Math)
- ? self
- : Function('return this')()
-;
-
-$.fn.tab = function(parameters) {
-
- var
- // use window context if none specified
- $allModules = $.isFunction(this)
- ? $(window)
- : $(this),
-
- moduleSelector = $allModules.selector || '',
- time = new Date().getTime(),
- performance = [],
-
- query = arguments[0],
- methodInvoked = (typeof query == 'string'),
- queryArguments = [].slice.call(arguments, 1),
-
- initializedHistory = false,
- returnedValue
- ;
-
- $allModules
- .each(function() {
- var
-
- settings = ( $.isPlainObject(parameters) )
- ? $.extend(true, {}, $.fn.tab.settings, parameters)
- : $.extend({}, $.fn.tab.settings),
-
- className = settings.className,
- metadata = settings.metadata,
- selector = settings.selector,
- error = settings.error,
- regExp = settings.regExp,
-
- eventNamespace = '.' + settings.namespace,
- moduleNamespace = 'module-' + settings.namespace,
-
- $module = $(this),
- $context,
- $tabs,
-
- cache = {},
- firstLoad = true,
- recursionDepth = 0,
- element = this,
- instance = $module.data(moduleNamespace),
-
- activeTabPath,
- parameterArray,
- module,
-
- historyEvent
-
- ;
-
- module = {
-
- initialize: function() {
- module.debug('Initializing tab menu item', $module);
- module.fix.callbacks();
- module.determineTabs();
-
- module.debug('Determining tabs', settings.context, $tabs);
- // set up automatic routing
- if(settings.auto) {
- module.set.auto();
- }
- module.bind.events();
-
- if(settings.history && !initializedHistory) {
- module.initializeHistory();
- initializedHistory = true;
- }
-
- if(settings.autoTabActivation && instance === undefined && module.determine.activeTab() == null) {
- module.debug('No active tab detected, setting first tab active', module.get.initialPath());
- module.changeTab(settings.autoTabActivation === true ? module.get.initialPath() : settings.autoTabActivation);
- };
-
- module.instantiate();
- },
-
- instantiate: function () {
- module.verbose('Storing instance of module', module);
- instance = module;
- $module
- .data(moduleNamespace, module)
- ;
- },
-
- destroy: function() {
- module.debug('Destroying tabs', $module);
- $module
- .removeData(moduleNamespace)
- .off(eventNamespace)
- ;
- },
-
- bind: {
- events: function() {
- // if using $.tab don't add events
- if( !$.isWindow( element ) ) {
- module.debug('Attaching tab activation events to element', $module);
- $module
- .on('click' + eventNamespace, module.event.click)
- ;
- }
- }
- },
-
- determineTabs: function() {
- var
- $reference
- ;
-
- // determine tab context
- if(settings.context === 'parent') {
- if($module.closest(selector.ui).length > 0) {
- $reference = $module.closest(selector.ui);
- module.verbose('Using closest UI element as parent', $reference);
- }
- else {
- $reference = $module;
- }
- $context = $reference.parent();
- module.verbose('Determined parent element for creating context', $context);
- }
- else if(settings.context) {
- $context = $(settings.context);
- module.verbose('Using selector for tab context', settings.context, $context);
- }
- else {
- $context = $('body');
- }
- // find tabs
- if(settings.childrenOnly) {
- $tabs = $context.children(selector.tabs);
- module.debug('Searching tab context children for tabs', $context, $tabs);
- }
- else {
- $tabs = $context.find(selector.tabs);
- module.debug('Searching tab context for tabs', $context, $tabs);
- }
- },
-
- fix: {
- callbacks: function() {
- if( $.isPlainObject(parameters) && (parameters.onTabLoad || parameters.onTabInit) ) {
- if(parameters.onTabLoad) {
- parameters.onLoad = parameters.onTabLoad;
- delete parameters.onTabLoad;
- module.error(error.legacyLoad, parameters.onLoad);
- }
- if(parameters.onTabInit) {
- parameters.onFirstLoad = parameters.onTabInit;
- delete parameters.onTabInit;
- module.error(error.legacyInit, parameters.onFirstLoad);
- }
- settings = $.extend(true, {}, $.fn.tab.settings, parameters);
- }
- }
- },
-
- initializeHistory: function() {
- module.debug('Initializing page state');
- if( $.address === undefined ) {
- module.error(error.state);
- return false;
- }
- else {
- if(settings.historyType == 'state') {
- module.debug('Using HTML5 to manage state');
- if(settings.path !== false) {
- $.address
- .history(true)
- .state(settings.path)
- ;
- }
- else {
- module.error(error.path);
- return false;
- }
- }
- $.address
- .bind('change', module.event.history.change)
- ;
- }
- },
-
- event: {
- click: function(event) {
- var
- tabPath = $(this).data(metadata.tab)
- ;
- if(tabPath !== undefined) {
- if(settings.history) {
- module.verbose('Updating page state', event);
- $.address.value(tabPath);
- }
- else {
- module.verbose('Changing tab', event);
- module.changeTab(tabPath);
- }
- event.preventDefault();
- }
- else {
- module.debug('No tab specified');
- }
- },
- history: {
- change: function(event) {
- var
- tabPath = event.pathNames.join('/') || module.get.initialPath(),
- pageTitle = settings.templates.determineTitle(tabPath) || false
- ;
- module.performance.display();
- module.debug('History change event', tabPath, event);
- historyEvent = event;
- if(tabPath !== undefined) {
- module.changeTab(tabPath);
- }
- if(pageTitle) {
- $.address.title(pageTitle);
- }
- }
- }
- },
-
- refresh: function() {
- if(activeTabPath) {
- module.debug('Refreshing tab', activeTabPath);
- module.changeTab(activeTabPath);
- }
- },
-
- cache: {
-
- read: function(cacheKey) {
- return (cacheKey !== undefined)
- ? cache[cacheKey]
- : false
- ;
- },
- add: function(cacheKey, content) {
- cacheKey = cacheKey || activeTabPath;
- module.debug('Adding cached content for', cacheKey);
- cache[cacheKey] = content;
- },
- remove: function(cacheKey) {
- cacheKey = cacheKey || activeTabPath;
- module.debug('Removing cached content for', cacheKey);
- delete cache[cacheKey];
- }
- },
-
- escape: {
- string: function(text) {
- text = String(text);
- return text.replace(regExp.escape, '\\$&');
- }
- },
-
- set: {
- auto: function() {
- var
- url = (typeof settings.path == 'string')
- ? settings.path.replace(/\/$/, '') + '/{$tab}'
- : '/{$tab}'
- ;
- module.verbose('Setting up automatic tab retrieval from server', url);
- if($.isPlainObject(settings.apiSettings)) {
- settings.apiSettings.url = url;
- }
- else {
- settings.apiSettings = {
- url: url
- };
- }
- },
- loading: function(tabPath) {
- var
- $tab = module.get.tabElement(tabPath),
- isLoading = $tab.hasClass(className.loading)
- ;
- if(!isLoading) {
- module.verbose('Setting loading state for', $tab);
- $tab
- .addClass(className.loading)
- .siblings($tabs)
- .removeClass(className.active + ' ' + className.loading)
- ;
- if($tab.length > 0) {
- settings.onRequest.call($tab[0], tabPath);
- }
- }
- },
- state: function(state) {
- $.address.value(state);
- }
- },
-
- changeTab: function(tabPath) {
- var
- pushStateAvailable = (window.history && window.history.pushState),
- shouldIgnoreLoad = (pushStateAvailable && settings.ignoreFirstLoad && firstLoad),
- remoteContent = (settings.auto || $.isPlainObject(settings.apiSettings) ),
- // only add default path if not remote content
- pathArray = (remoteContent && !shouldIgnoreLoad)
- ? module.utilities.pathToArray(tabPath)
- : module.get.defaultPathArray(tabPath)
- ;
- tabPath = module.utilities.arrayToPath(pathArray);
- $.each(pathArray, function(index, tab) {
- var
- currentPathArray = pathArray.slice(0, index + 1),
- currentPath = module.utilities.arrayToPath(currentPathArray),
-
- isTab = module.is.tab(currentPath),
- isLastIndex = (index + 1 == pathArray.length),
-
- $tab = module.get.tabElement(currentPath),
- $anchor,
- nextPathArray,
- nextPath,
- isLastTab
- ;
- module.verbose('Looking for tab', tab);
- if(isTab) {
- module.verbose('Tab was found', tab);
- // scope up
- activeTabPath = currentPath;
- parameterArray = module.utilities.filterArray(pathArray, currentPathArray);
-
- if(isLastIndex) {
- isLastTab = true;
- }
- else {
- nextPathArray = pathArray.slice(0, index + 2);
- nextPath = module.utilities.arrayToPath(nextPathArray);
- isLastTab = ( !module.is.tab(nextPath) );
- if(isLastTab) {
- module.verbose('Tab parameters found', nextPathArray);
- }
- }
- if(isLastTab && remoteContent) {
- if(!shouldIgnoreLoad) {
- module.activate.navigation(currentPath);
- module.fetch.content(currentPath, tabPath);
- }
- else {
- module.debug('Ignoring remote content on first tab load', currentPath);
- firstLoad = false;
- module.cache.add(tabPath, $tab.html());
- module.activate.all(currentPath);
- settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent);
- settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent);
- }
- return false;
- }
- else {
- module.debug('Opened local tab', currentPath);
- module.activate.all(currentPath);
- if( !module.cache.read(currentPath) ) {
- module.cache.add(currentPath, true);
- module.debug('First time tab loaded calling tab init');
- settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent);
- }
- settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent);
- }
-
- }
- else if(tabPath.search('/') == -1 && tabPath !== '') {
- // look for in page anchor
- tabPath = module.escape.string(tabPath);
- $anchor = $('#' + tabPath + ', a[name="' + tabPath + '"]');
- currentPath = $anchor.closest('[data-tab]').data(metadata.tab);
- $tab = module.get.tabElement(currentPath);
- // if anchor exists use parent tab
- if($anchor && $anchor.length > 0 && currentPath) {
- module.debug('Anchor link used, opening parent tab', $tab, $anchor);
- if( !$tab.hasClass(className.active) ) {
- setTimeout(function() {
- module.scrollTo($anchor);
- }, 0);
- }
- module.activate.all(currentPath);
- if( !module.cache.read(currentPath) ) {
- module.cache.add(currentPath, true);
- module.debug('First time tab loaded calling tab init');
- settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent);
- }
- settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent);
- return false;
- }
- }
- else {
- module.error(error.missingTab, $module, $context, currentPath);
- return false;
- }
- });
- },
-
- scrollTo: function($element) {
- var
- scrollOffset = ($element && $element.length > 0)
- ? $element.offset().top
- : false
- ;
- if(scrollOffset !== false) {
- module.debug('Forcing scroll to an in-page link in a hidden tab', scrollOffset, $element);
- $(document).scrollTop(scrollOffset);
- }
- },
-
- update: {
- content: function(tabPath, html, evaluateScripts) {
- var
- $tab = module.get.tabElement(tabPath),
- tab = $tab[0]
- ;
- evaluateScripts = (evaluateScripts !== undefined)
- ? evaluateScripts
- : settings.evaluateScripts
- ;
- if(typeof settings.cacheType == 'string' && settings.cacheType.toLowerCase() == 'dom' && typeof html !== 'string') {
- $tab
- .empty()
- .append($(html).clone(true))
- ;
- }
- else {
- if(evaluateScripts) {
- module.debug('Updating HTML and evaluating inline scripts', tabPath, html);
- $tab.html(html);
- }
- else {
- module.debug('Updating HTML', tabPath, html);
- tab.innerHTML = html;
- }
- }
- }
- },
-
- fetch: {
-
- content: function(tabPath, fullTabPath) {
- var
- $tab = module.get.tabElement(tabPath),
- apiSettings = {
- dataType : 'html',
- encodeParameters : false,
- on : 'now',
- cache : settings.alwaysRefresh,
- headers : {
- 'X-Remote': true
- },
- onSuccess : function(response) {
- if(settings.cacheType == 'response') {
- module.cache.add(fullTabPath, response);
- }
- module.update.content(tabPath, response);
- if(tabPath == activeTabPath) {
- module.debug('Content loaded', tabPath);
- module.activate.tab(tabPath);
- }
- else {
- module.debug('Content loaded in background', tabPath);
- }
- settings.onFirstLoad.call($tab[0], tabPath, parameterArray, historyEvent);
- settings.onLoad.call($tab[0], tabPath, parameterArray, historyEvent);
-
- if(settings.loadOnce) {
- module.cache.add(fullTabPath, true);
- }
- else if(typeof settings.cacheType == 'string' && settings.cacheType.toLowerCase() == 'dom' && $tab.children().length > 0) {
- setTimeout(function() {
- var
- $clone = $tab.children().clone(true)
- ;
- $clone = $clone.not('script');
- module.cache.add(fullTabPath, $clone);
- }, 0);
- }
- else {
- module.cache.add(fullTabPath, $tab.html());
- }
- },
- urlData: {
- tab: fullTabPath
- }
- },
- request = $tab.api('get request') || false,
- existingRequest = ( request && request.state() === 'pending' ),
- requestSettings,
- cachedContent
- ;
-
- fullTabPath = fullTabPath || tabPath;
- cachedContent = module.cache.read(fullTabPath);
-
-
- if(settings.cache && cachedContent) {
- module.activate.tab(tabPath);
- module.debug('Adding cached content', fullTabPath);
- if(!settings.loadOnce) {
- if(settings.evaluateScripts == 'once') {
- module.update.content(tabPath, cachedContent, false);
- }
- else {
- module.update.content(tabPath, cachedContent);
- }
- }
- settings.onLoad.call($tab[0], tabPath, parameterArray, historyEvent);
- }
- else if(existingRequest) {
- module.set.loading(tabPath);
- module.debug('Content is already loading', fullTabPath);
- }
- else if($.api !== undefined) {
- requestSettings = $.extend(true, {}, settings.apiSettings, apiSettings);
- module.debug('Retrieving remote content', fullTabPath, requestSettings);
- module.set.loading(tabPath);
- $tab.api(requestSettings);
- }
- else {
- module.error(error.api);
- }
- }
- },
-
- activate: {
- all: function(tabPath) {
- module.activate.tab(tabPath);
- module.activate.navigation(tabPath);
- },
- tab: function(tabPath) {
- var
- $tab = module.get.tabElement(tabPath),
- $deactiveTabs = (settings.deactivate == 'siblings')
- ? $tab.siblings($tabs)
- : $tabs.not($tab),
- isActive = $tab.hasClass(className.active)
- ;
- module.verbose('Showing tab content for', $tab);
- if(!isActive) {
- $tab
- .addClass(className.active)
- ;
- $deactiveTabs
- .removeClass(className.active + ' ' + className.loading)
- ;
- if($tab.length > 0) {
- settings.onVisible.call($tab[0], tabPath);
- }
- }
- },
- navigation: function(tabPath) {
- var
- $navigation = module.get.navElement(tabPath),
- $deactiveNavigation = (settings.deactivate == 'siblings')
- ? $navigation.siblings($allModules)
- : $allModules.not($navigation),
- isActive = $navigation.hasClass(className.active)
- ;
- module.verbose('Activating tab navigation for', $navigation, tabPath);
- if(!isActive) {
- $navigation
- .addClass(className.active)
- ;
- $deactiveNavigation
- .removeClass(className.active + ' ' + className.loading)
- ;
- }
- }
- },
-
- deactivate: {
- all: function() {
- module.deactivate.navigation();
- module.deactivate.tabs();
- },
- navigation: function() {
- $allModules
- .removeClass(className.active)
- ;
- },
- tabs: function() {
- $tabs
- .removeClass(className.active + ' ' + className.loading)
- ;
- }
- },
-
- is: {
- tab: function(tabName) {
- return (tabName !== undefined)
- ? ( module.get.tabElement(tabName).length > 0 )
- : false
- ;
- }
- },
-
- get: {
- initialPath: function() {
- return $allModules.eq(0).data(metadata.tab) || $tabs.eq(0).data(metadata.tab);
- },
- path: function() {
- return $.address.value();
- },
- // adds default tabs to tab path
- defaultPathArray: function(tabPath) {
- return module.utilities.pathToArray( module.get.defaultPath(tabPath) );
- },
- defaultPath: function(tabPath) {
- var
- $defaultNav = $allModules.filter('[data-' + metadata.tab + '^="' + module.escape.string(tabPath) + '/"]').eq(0),
- defaultTab = $defaultNav.data(metadata.tab) || false
- ;
- if( defaultTab ) {
- module.debug('Found default tab', defaultTab);
- if(recursionDepth < settings.maxDepth) {
- recursionDepth++;
- return module.get.defaultPath(defaultTab);
- }
- module.error(error.recursion);
- }
- else {
- module.debug('No default tabs found for', tabPath, $tabs);
- }
- recursionDepth = 0;
- return tabPath;
- },
- navElement: function(tabPath) {
- tabPath = tabPath || activeTabPath;
- return $allModules.filter('[data-' + metadata.tab + '="' + module.escape.string(tabPath) + '"]');
- },
- tabElement: function(tabPath) {
- var
- $fullPathTab,
- $simplePathTab,
- tabPathArray,
- lastTab
- ;
- tabPath = tabPath || activeTabPath;
- tabPathArray = module.utilities.pathToArray(tabPath);
- lastTab = module.utilities.last(tabPathArray);
- $fullPathTab = $tabs.filter('[data-' + metadata.tab + '="' + module.escape.string(tabPath) + '"]');
- $simplePathTab = $tabs.filter('[data-' + metadata.tab + '="' + module.escape.string(lastTab) + '"]');
- return ($fullPathTab.length > 0)
- ? $fullPathTab
- : $simplePathTab
- ;
- },
- tab: function() {
- return activeTabPath;
- }
- },
-
- determine: {
- activeTab: function() {
- var activeTab = null;
-
- $tabs.each(function(_index, tab) {
- var $tab = $(tab);
-
- if( $tab.hasClass(className.active) ) {
- var
- tabPath = $(this).data(metadata.tab),
- $anchor = $allModules.filter('[data-' + metadata.tab + '="' + module.escape.string(tabPath) + '"]')
- ;
-
- if( $anchor.hasClass(className.active) ) {
- activeTab = tabPath;
- }
- }
- });
-
- return activeTab;
- }
- },
-
- utilities: {
- filterArray: function(keepArray, removeArray) {
- return $.grep(keepArray, function(keepValue) {
- return ( $.inArray(keepValue, removeArray) == -1);
- });
- },
- last: function(array) {
- return Array.isArray(array)
- ? array[ array.length - 1]
- : false
- ;
- },
- pathToArray: function(pathName) {
- if(pathName === undefined) {
- pathName = activeTabPath;
- }
- return typeof pathName == 'string'
- ? pathName.split('/')
- : [pathName]
- ;
- },
- arrayToPath: function(pathArray) {
- return Array.isArray(pathArray)
- ? pathArray.join('/')
- : false
- ;
- }
- },
-
- setting: function(name, value) {
- module.debug('Changing setting', name, value);
- if( $.isPlainObject(name) ) {
- $.extend(true, settings, name);
- }
- else if(value !== undefined) {
- if($.isPlainObject(settings[name])) {
- $.extend(true, settings[name], value);
- }
- else {
- settings[name] = value;
- }
- }
- else {
- return settings[name];
- }
- },
- internal: function(name, value) {
- if( $.isPlainObject(name) ) {
- $.extend(true, module, name);
- }
- else if(value !== undefined) {
- module[name] = value;
- }
- else {
- return module[name];
- }
- },
- debug: function() {
- if(!settings.silent && settings.debug) {
- if(settings.performance) {
- module.performance.log(arguments);
- }
- else {
- module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
- module.debug.apply(console, arguments);
- }
- }
- },
- verbose: function() {
- if(!settings.silent && settings.verbose && settings.debug) {
- if(settings.performance) {
- module.performance.log(arguments);
- }
- else {
- module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
- module.verbose.apply(console, arguments);
- }
- }
- },
- error: function() {
- if(!settings.silent) {
- module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
- module.error.apply(console, arguments);
- }
- },
- performance: {
- log: function(message) {
- var
- currentTime,
- executionTime,
- previousTime
- ;
- if(settings.performance) {
- currentTime = new Date().getTime();
- previousTime = time || currentTime;
- executionTime = currentTime - previousTime;
- time = currentTime;
- performance.push({
- 'Name' : message[0],
- 'Arguments' : [].slice.call(message, 1) || '',
- 'Element' : element,
- 'Execution Time' : executionTime
- });
- }
- clearTimeout(module.performance.timer);
- module.performance.timer = setTimeout(module.performance.display, 500);
- },
- display: function() {
- var
- title = settings.name + ':',
- totalTime = 0
- ;
- time = false;
- clearTimeout(module.performance.timer);
- $.each(performance, function(index, data) {
- totalTime += data['Execution Time'];
- });
- title += ' ' + totalTime + 'ms';
- if(moduleSelector) {
- title += ' \'' + moduleSelector + '\'';
- }
- if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
- console.groupCollapsed(title);
- if(console.table) {
- console.table(performance);
- }
- else {
- $.each(performance, function(index, data) {
- console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
- });
- }
- console.groupEnd();
- }
- performance = [];
- }
- },
- invoke: function(query, passedArguments, context) {
- var
- object = instance,
- maxDepth,
- found,
- response
- ;
- passedArguments = passedArguments || queryArguments;
- context = element || context;
- if(typeof query == 'string' && object !== undefined) {
- query = query.split(/[\. ]/);
- maxDepth = query.length - 1;
- $.each(query, function(depth, value) {
- var camelCaseValue = (depth != maxDepth)
- ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
- : query
- ;
- if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
- object = object[camelCaseValue];
- }
- else if( object[camelCaseValue] !== undefined ) {
- found = object[camelCaseValue];
- return false;
- }
- else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
- object = object[value];
- }
- else if( object[value] !== undefined ) {
- found = object[value];
- return false;
- }
- else {
- module.error(error.method, query);
- return false;
- }
- });
- }
- if ( $.isFunction( found ) ) {
- response = found.apply(context, passedArguments);
- }
- else if(found !== undefined) {
- response = found;
- }
- if(Array.isArray(returnedValue)) {
- returnedValue.push(response);
- }
- else if(returnedValue !== undefined) {
- returnedValue = [returnedValue, response];
- }
- else if(response !== undefined) {
- returnedValue = response;
- }
- return found;
- }
- };
- if(methodInvoked) {
- if(instance === undefined) {
- module.initialize();
- }
- module.invoke(query);
- }
- else {
- if(instance !== undefined) {
- instance.invoke('destroy');
- }
- module.initialize();
- }
- })
- ;
- return (returnedValue !== undefined)
- ? returnedValue
- : this
- ;
-
-};
-
-// shortcut for tabbed content with no defined navigation
-$.tab = function() {
- $(window).tab.apply(this, arguments);
-};
-
-$.fn.tab.settings = {
-
- name : 'Tab',
- namespace : 'tab',
-
- silent : false,
- debug : false,
- verbose : false,
- performance : true,
-
- auto : false, // uses pjax style endpoints fetching content from same url with remote-content headers
- history : false, // use browser history
- historyType : 'hash', // #/ or html5 state
- path : false, // base path of url
-
- context : false, // specify a context that tabs must appear inside
- childrenOnly : false, // use only tabs that are children of context
- maxDepth : 25, // max depth a tab can be nested
-
- deactivate : 'siblings', // whether tabs should deactivate sibling menu elements or all elements initialized together
-
- alwaysRefresh : false, // load tab content new every tab click
- cache : true, // cache the content requests to pull locally
- loadOnce : false, // Whether tab data should only be loaded once when using remote content
- cacheType : 'response', // Whether to cache exact response, or to html cache contents after scripts execute
- ignoreFirstLoad : false, // don't load remote content on first load
-
- apiSettings : false, // settings for api call
- evaluateScripts : 'once', // whether inline scripts should be parsed (true/false/once). Once will not re-evaluate on cached content
- autoTabActivation: true, // whether a non existing active tab will auto activate the first available tab
-
- onFirstLoad : function(tabPath, parameterArray, historyEvent) {}, // called first time loaded
- onLoad : function(tabPath, parameterArray, historyEvent) {}, // called on every load
- onVisible : function(tabPath, parameterArray, historyEvent) {}, // called every time tab visible
- onRequest : function(tabPath, parameterArray, historyEvent) {}, // called ever time a tab beings loading remote content
-
- templates : {
- determineTitle: function(tabArray) {} // returns page title for path
- },
-
- error: {
- api : 'You attempted to load content without API module',
- method : 'The method you called is not defined',
- missingTab : 'Activated tab cannot be found. Tabs are case-sensitive.',
- noContent : 'The tab you specified is missing a content url.',
- path : 'History enabled, but no path was specified',
- recursion : 'Max recursive depth reached',
- legacyInit : 'onTabInit has been renamed to onFirstLoad in 2.0, please adjust your code.',
- legacyLoad : 'onTabLoad has been renamed to onLoad in 2.0. Please adjust your code',
- state : 'History requires Asual\'s Address library '
- },
-
- regExp : {
- escape : /[-[\]{}()*+?.,\\^$|#\s:=@]/g
- },
-
- metadata : {
- tab : 'tab',
- loaded : 'loaded',
- promise: 'promise'
- },
-
- className : {
- loading : 'loading',
- active : 'active'
- },
-
- selector : {
- tabs : '.ui.tab',
- ui : '.ui'
- }
-
-};
-
-})( jQuery, window, document );
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 69474b7539..14afba2193 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -27,7 +27,6 @@
"form",
"menu",
"modal",
- "search",
- "tab"
+ "search"
]
}
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 2102e995d8..a99af0ef25 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -12,6 +12,7 @@ import {confirmModal} from './comp/ConfirmModal.js';
import {showErrorToast} from '../modules/toast.js';
import {request, POST, GET} from '../modules/fetch.js';
import '../htmx.js';
+import {initTab} from '../modules/tab.ts';
const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
@@ -195,7 +196,9 @@ export function initGlobalCommon() {
$uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward');
$uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward');
- $('.tabular.menu .item').tab();
+ for (const el of document.querySelectorAll('.tabular.menu')) {
+ initTab(el);
+ }
initSubmitEventPolyfill();
document.addEventListener('submit', formFetchAction);
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index 4edd13bfc2..d96628c644 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -10,6 +10,7 @@ import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
import {initTextExpander} from './TextExpander.js';
import {showErrorToast, showHintToast} from '../../modules/toast.js';
import {POST} from '../../modules/fetch.js';
+import {initTab} from '../../modules/tab.ts';
/**
* validate if the given textarea is non-empty.
@@ -200,7 +201,8 @@ class ComboMarkdownEditor {
setupTab() {
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.
// 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.previewContext = tabPreviewer.getAttribute('data-preview-context');
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index d3f5f2f8a9..ac4fc8a75e 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -5,10 +5,11 @@ import {hideElem, showElem, createElementFromHTML} from '../utils/dom.js';
import {initMarkupContent} from '../markup/content.js';
import {attachRefIssueContextPopup} from './contextpopup.js';
import {POST} from '../modules/fetch.js';
+import {initTab} from '../modules/tab.ts';
function initEditPreviewTab($form) {
const $tabMenu = $form.find('.tabular.menu');
- $tabMenu.find('.item').tab();
+ initTab($tabMenu[0]);
const $previewTab = $tabMenu.find(
`.item[data-tab="${$tabMenu.data('preview')}"]`,
);
diff --git a/web_src/js/modules/fomantic.js b/web_src/js/modules/fomantic.js
index c736026c6c..9d27a9bb47 100644
--- a/web_src/js/modules/fomantic.js
+++ b/web_src/js/modules/fomantic.js
@@ -11,9 +11,6 @@ import {initDimmer} from './dimmer.ts';
export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)');
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
$.fn.dropdown.settings.fullTextSearch = 'exact';
// Do not use "cursor: pointer" for dropdown labels
diff --git a/web_src/js/modules/tab.ts b/web_src/js/modules/tab.ts
new file mode 100644
index 0000000000..a19281793f
--- /dev/null
+++ b/web_src/js/modules/tab.ts
@@ -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});
+ }
+}