From 582f911e8f6397d9d66b15bc3c89ab62417967d0 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 25 Mar 2025 22:41:58 +0800 Subject: [PATCH 1/4] fix --- web_src/js/modules/fomantic/dropdown.ts | 26 +++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 8736e041df7ca..d71562f3b4321 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -15,20 +15,26 @@ export function initAriaDropdownPatch() { } // the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and: -// * it does the one-time attaching on the first call -// * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes +// * it does the one-time element event attaching on the first call +// * it delegates the module internal functions like `onLabelCreate` to the patched functions to add more features. function ariaDropdownFn(this: any, ...args: Parameters) { const ret = fomanticDropdownFn.apply(this, args); - // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument, - // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks. - const needDelegate = (!args.length || typeof args[0] !== 'string'); for (const el of this) { if (!el[ariaPatchKey]) { - attachInit(el); + // the elements don't belong to the dropdown "module" and won't be reset + // so we only need to initialize them once. + attachInitElements(el); } - if (needDelegate) { - delegateOne($(el)); + + // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument, + // it means that such call will reset the dropdown "module" including internal settings, + // then we need to re-delegate the callbacks. + const $dropdown = $(el); + const dropdownModule = $dropdown.data('module-dropdown'); + if (!dropdownModule.giteaDelegated) { + dropdownModule.giteaDelegated = true; + delegateDropdownModule($dropdown); } } return ret; @@ -68,7 +74,7 @@ function processMenuItems($dropdown: any, dropdownCall: any) { } // delegate the dropdown's template functions and callback functions to add aria attributes. -function delegateOne($dropdown: any) { +function delegateDropdownModule($dropdown: any) { const dropdownCall = fomanticDropdownFn.bind($dropdown); // If there is a "search input" in the "menu", Fomantic will only "focus the input" but not "toggle the menu" when the "dropdown icon" is clicked. @@ -163,7 +169,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men } } -function attachInit(dropdown: HTMLElement) { +function attachInitElements(dropdown: HTMLElement) { (dropdown as any)[ariaPatchKey] = {}; if (dropdown.classList.contains('custom')) return; From 70746e771a2c87f14c680b729544f2c35ae433cf Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 26 Mar 2025 08:39:18 +0800 Subject: [PATCH 2/4] fix more --- modules/templates/util_avatar.go | 2 +- templates/repo/icon.tmpl | 2 +- web_src/js/modules/fomantic/dropdown.ts | 10 ---------- web_src/js/modules/observer.ts | 4 +++- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go index f7dd408ee2133..09da2496e86dc 100644 --- a/modules/templates/util_avatar.go +++ b/modules/templates/util_avatar.go @@ -34,7 +34,7 @@ func AvatarHTML(src string, size int, class, name string) template.HTML { name = "avatar" } - return template.HTML(``) + return template.HTML(``) } // Avatar renders user avatars. args: user, size (int), class (string) diff --git a/templates/repo/icon.tmpl b/templates/repo/icon.tmpl index e5e0bd68e785e..3747d3a6f5b0d 100644 --- a/templates/repo/icon.tmpl +++ b/templates/repo/icon.tmpl @@ -1,6 +1,6 @@ {{$avatarLink := (.RelAvatarLink ctx)}} {{if $avatarLink}} - {{.FullName}} + {{else if $.IsMirror}} {{svg "octicon-mirror" 24}} {{else if $.IsFork}} diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index d71562f3b4321..b4a5b8a42be1b 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -77,15 +77,6 @@ function processMenuItems($dropdown: any, dropdownCall: any) { function delegateDropdownModule($dropdown: any) { const dropdownCall = fomanticDropdownFn.bind($dropdown); - // If there is a "search input" in the "menu", Fomantic will only "focus the input" but not "toggle the menu" when the "dropdown icon" is clicked. - // Actually, Fomantic UI doesn't support such layout/usage. It needs to patch the "focusSearch" / "blurSearch" functions to make sure it toggles the menu. - const oldFocusSearch = dropdownCall('internal', 'focusSearch'); - const oldBlurSearch = dropdownCall('internal', 'blurSearch'); - // * If the "dropdown icon" is clicked, Fomantic calls "focusSearch", so show the menu - dropdownCall('internal', 'focusSearch', function (this: any) { dropdownCall('show'); oldFocusSearch.call(this) }); - // * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu - dropdownCall('internal', 'blurSearch', function (this: any) { oldBlurSearch.call(this); dropdownCall('hide') }); - const oldFilterItems = dropdownCall('internal', 'filterItems'); dropdownCall('internal', 'filterItems', function (this: any, ...args: any[]) { oldFilterItems.call(this, ...args); @@ -171,7 +162,6 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men function attachInitElements(dropdown: HTMLElement) { (dropdown as any)[ariaPatchKey] = {}; - if (dropdown.classList.contains('custom')) return; // Dropdown has 2 different focusing behaviors // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element. diff --git a/web_src/js/modules/observer.ts b/web_src/js/modules/observer.ts index 06208d0507f9c..3305c2f29dec4 100644 --- a/web_src/js/modules/observer.ts +++ b/web_src/js/modules/observer.ts @@ -46,9 +46,11 @@ function callGlobalInitFunc(el: HTMLElement) { const func = globalInitFuncs[initFunc]; if (!func) throw new Error(`Global init function "${initFunc}" not found`); + // when an element node is removed and added again, it should not be re-initialized again. type GiteaGlobalInitElement = Partial & {_giteaGlobalInited: boolean}; - if ((el as GiteaGlobalInitElement)._giteaGlobalInited) throw new Error(`Global init function "${initFunc}" already executed`); + if ((el as GiteaGlobalInitElement)._giteaGlobalInited) return; (el as GiteaGlobalInitElement)._giteaGlobalInited = true; + func(el); } From 6b7f6eeb2356bb81c314544e6ba24e6202b244b7 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 26 Mar 2025 09:58:45 +0800 Subject: [PATCH 3/4] fix --- web_src/fomantic/build/components/dropdown.js | 3 ++ web_src/js/modules/fomantic/dropdown.test.ts | 26 +++++++++++-- web_src/js/modules/fomantic/dropdown.ts | 39 ++++++++----------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index 009b51d8b1fd9..163de9f751d10 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -752,6 +752,7 @@ $.fn.dropdown = function(parameters) { if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { module.show(); } + settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items } ; if(settings.useLabels && module.has.maxSelections()) { @@ -3992,6 +3993,8 @@ $.fn.dropdown.settings = { onShow : function(){}, onHide : function(){}, + onAfterFiltered: function(){}, // GITEA-PATCH: callback to correctly handle the filtered items + /* Component */ name : 'Dropdown', namespace : 'dropdown', diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts index 587e0bca7c63f..1b94c91a12dc3 100644 --- a/web_src/js/modules/fomantic/dropdown.test.ts +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -15,15 +15,35 @@ test('hideScopedEmptyDividers-simple', () => { expect(container.innerHTML).toEqual(`
a
+
-
b
`); }); -test('hideScopedEmptyDividers-hidden1', () => { +test('hideScopedEmptyDividers-items-all-filtered', () => { + const container = createElementFromHTML(`
+
+
+
a
+
b
+
+
+
`); + hideScopedEmptyDividers(container); + expect(container.innerHTML).toEqual(` +
+
+
a
+
b
+ +
+`); +}); + +test('hideScopedEmptyDividers-hide-last', () => { const container = createElementFromHTML(`
a
@@ -37,7 +57,7 @@ test('hideScopedEmptyDividers-hidden1', () => { `); }); -test('hideScopedEmptyDividers-hidden2', () => { +test('hideScopedEmptyDividers-scoped-items', () => { const container = createElementFromHTML(`
a
diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index b4a5b8a42be1b..d15f8d338583b 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -9,6 +9,7 @@ const fomanticDropdownFn = $.fn.dropdown; // use our own `$().dropdown` function to patch Fomantic's dropdown module export function initAriaDropdownPatch() { if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); + $.fn.dropdown.settings.onAfterFiltered = onAfterFiltered; $.fn.dropdown = ariaDropdownFn; $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings; @@ -67,8 +68,9 @@ function updateSelectionLabel(label: HTMLElement) { } } -function processMenuItems($dropdown: any, dropdownCall: any) { - const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty'; +function onAfterFiltered(this: any) { + const $dropdown = $(this); + const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty'; const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); } @@ -77,18 +79,6 @@ function processMenuItems($dropdown: any, dropdownCall: any) { function delegateDropdownModule($dropdown: any) { const dropdownCall = fomanticDropdownFn.bind($dropdown); - const oldFilterItems = dropdownCall('internal', 'filterItems'); - dropdownCall('internal', 'filterItems', function (this: any, ...args: any[]) { - oldFilterItems.call(this, ...args); - processMenuItems($dropdown, dropdownCall); - }); - - const oldShow = dropdownCall('internal', 'show'); - dropdownCall('internal', 'show', function (this: any, ...args: any[]) { - oldShow.call(this, ...args); - processMenuItems($dropdown, dropdownCall); - }); - // the "template" functions are used for dynamic creation (eg: AJAX) const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()}; const dropdownTemplatesMenuOld = dropdownTemplates.menu; @@ -301,9 +291,11 @@ export function hideScopedEmptyDividers(container: Element) { const visibleItems: Element[] = []; const curScopeVisibleItems: Element[] = []; let curScope: string = '', lastVisibleScope: string = ''; - const isScopedDivider = (item: Element) => item.matches('.divider') && item.hasAttribute('data-scope'); + const isDivider = (item: Element) => item.classList.contains('divider'); + const isScopedDivider = (item: Element) => isDivider(item) && item.hasAttribute('data-scope'); const hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items - + const showDivider = (item: Element) => item.classList.remove('hidden', 'transition'); + const isHidden = (item: Element) => item.classList.contains('hidden') || item.classList.contains('filtered') || item.classList.contains('tw-hidden'); const handleScopeSwitch = (itemScope: string) => { if (curScopeVisibleItems.length === 1 && isScopedDivider(curScopeVisibleItems[0])) { hideDivider(curScopeVisibleItems[0]); @@ -319,13 +311,16 @@ export function hideScopedEmptyDividers(container: Element) { curScopeVisibleItems.length = 0; }; + // reset hidden dividers + queryElems(container, '.divider', showDivider); + // hide the scope dividers if the scope items are empty for (const item of container.children) { const itemScope = item.getAttribute('data-scope') || ''; if (itemScope !== curScope) { handleScopeSwitch(itemScope); } - if (!item.classList.contains('filtered') && !item.classList.contains('tw-hidden')) { + if (!isHidden(item)) { curScopeVisibleItems.push(item as HTMLElement); } } @@ -333,20 +328,20 @@ export function hideScopedEmptyDividers(container: Element) { // hide all leading and trailing dividers while (visibleItems.length) { - if (!visibleItems[0].matches('.divider')) break; + if (!isDivider(visibleItems[0])) break; hideDivider(visibleItems[0]); visibleItems.shift(); } while (visibleItems.length) { - if (!visibleItems[visibleItems.length - 1].matches('.divider')) break; + if (!isDivider(visibleItems[visibleItems.length - 1])) break; hideDivider(visibleItems[visibleItems.length - 1]); visibleItems.pop(); } // hide all duplicate dividers, hide current divider if next sibling is still divider // no need to update "visibleItems" array since this is the last loop - for (const item of visibleItems) { - if (!item.matches('.divider')) continue; - if (item.nextElementSibling?.matches('.divider')) hideDivider(item); + for (let i = 0; i < visibleItems.length - 1; i++) { + if (!visibleItems[i].matches('.divider')) continue; + if (visibleItems[i + 1].matches('.divider')) hideDivider(visibleItems[i + 1]); } } From 33eb1bd9c082bf91f666f1f29aedcf1957776f6b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 26 Mar 2025 10:23:21 +0800 Subject: [PATCH 4/4] fine tune --- modules/templates/util_avatar.go | 1 + web_src/js/modules/fomantic/dropdown.test.ts | 6 +++--- web_src/js/modules/fomantic/dropdown.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go index 09da2496e86dc..470e24fa6191f 100644 --- a/modules/templates/util_avatar.go +++ b/modules/templates/util_avatar.go @@ -34,6 +34,7 @@ func AvatarHTML(src string, size int, class, name string) template.HTML { name = "avatar" } + // use empty alt, otherwise if the image fails to load, the width will follow the "alt" text's width return template.HTML(``) } diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts index 1b94c91a12dc3..dd3497c8fce0e 100644 --- a/web_src/js/modules/fomantic/dropdown.test.ts +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -15,9 +15,9 @@ test('hideScopedEmptyDividers-simple', () => { expect(container.innerHTML).toEqual(`
a
-
+
b
`); @@ -35,10 +35,10 @@ test('hideScopedEmptyDividers-items-all-filtered', () => { hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(`
-
+
a
b
- +
`); }); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index d15f8d338583b..4bd08c5226363 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -341,7 +341,7 @@ export function hideScopedEmptyDividers(container: Element) { // no need to update "visibleItems" array since this is the last loop for (let i = 0; i < visibleItems.length - 1; i++) { if (!visibleItems[i].matches('.divider')) continue; - if (visibleItems[i + 1].matches('.divider')) hideDivider(visibleItems[i + 1]); + if (visibleItems[i + 1].matches('.divider')) hideDivider(visibleItems[i]); } }