aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2024-12-09 15:54:59 +0800
committerGitHub <noreply@github.com>2024-12-09 07:54:59 +0000
commit5675efb3e015fb8d87d86d1b79de2200f7dfb9af (patch)
tree5c5ad18d8a7fd31632f9d2f1dd4e6a65011c5271 /web_src/js
parent2d13eafd69b368bc75faa25d7ecfbda98a0792f8 (diff)
downloadgitea-5675efb3e015fb8d87d86d1b79de2200f7dfb9af.tar.gz
gitea-5675efb3e015fb8d87d86d1b79de2200f7dfb9af.zip
Fix duplicate dropdown dividers (#32760)
Fix #27466 The problem is that any item in the menu could be hidden, pure CSS won't work, and dropdown's builtin "hideDividers" doesn't work with our "scope dividers". The newly introduced "archived" label makes the dividers regression more.
Diffstat (limited to 'web_src/js')
-rw-r--r--web_src/js/features/repo-issue.ts56
-rw-r--r--web_src/js/index.ts3
-rw-r--r--web_src/js/modules/fomantic/dropdown.test.ts56
-rw-r--r--web_src/js/modules/fomantic/dropdown.ts80
-rw-r--r--web_src/js/webcomponents/overflow-menu.ts2
5 files changed, 167 insertions, 30 deletions
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index 477edbeb5f..f5a36b7717 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -8,6 +8,7 @@ import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts';
import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
+import {fomanticQuery} from '../modules/fomantic/base.ts';
const {appSubUrl} = window.config;
@@ -31,34 +32,35 @@ export function initRepoIssueSidebarList() {
if (crossRepoSearch === 'true') {
issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${issuePageInfo.repoId}&type=${issuePageInfo.issueDependencySearchType}`;
}
- $('#new-dependency-drop-list')
- .dropdown({
- apiSettings: {
- url: issueSearchUrl,
- onResponse(response) {
- const filteredResponse = {success: true, results: []};
- const currIssueId = $('#new-dependency-drop-list').data('issue-id');
- // Parse the response from the api to work with our dropdown
- $.each(response, (_i, issue) => {
- // Don't list current issue in the dependency list.
- if (issue.id === currIssueId) {
- return;
- }
- filteredResponse.results.push({
- name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div>
+ fomanticQuery('#new-dependency-drop-list').dropdown({
+ fullTextSearch: true,
+ apiSettings: {
+ url: issueSearchUrl,
+ onResponse(response) {
+ const filteredResponse = {success: true, results: []};
+ const currIssueId = $('#new-dependency-drop-list').data('issue-id');
+ // Parse the response from the api to work with our dropdown
+ $.each(response, (_i, issue) => {
+ // Don't list current issue in the dependency list.
+ if (issue.id === currIssueId) {
+ return;
+ }
+ filteredResponse.results.push({
+ name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div>
<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
- value: issue.id,
- });
+ value: issue.id,
});
- return filteredResponse;
- },
- cache: false,
+ });
+ return filteredResponse;
},
+ cache: false,
+ },
+ });
+}
- fullTextSearch: true,
- });
-
- $('.menu a.label-filter-item').each(function () {
+export function initRepoIssueLabelFilter() {
+ // the "label-filter" is used in 2 templates: projects/view, issue/filter_list (issue list page including the milestone page)
+ $('.ui.dropdown.label-filter a.label-filter-item').each(function () {
$(this).on('click', function (e) {
if (e.altKey) {
e.preventDefault();
@@ -66,11 +68,9 @@ export function initRepoIssueSidebarList() {
}
});
});
-
- // FIXME: it is wrong place to init ".ui.dropdown.label-filter"
- $('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
+ $('.ui.dropdown.label-filter').on('keydown', (e) => {
if (e.altKey && e.key === 'Enter') {
- const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
+ const selectedItem = document.querySelector('.ui.dropdown.label-filter .menu .item.selected');
if (selectedItem) {
excludeLabel(selectedItem);
}
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index f93c3495af..2964ef5572 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -29,7 +29,7 @@ import {
initRepoIssueWipTitle,
initRepoPullRequestMergeInstruction,
initRepoPullRequestAllowMaintainerEdit,
- initRepoPullRequestReview, initRepoIssueSidebarList,
+ initRepoPullRequestReview, initRepoIssueSidebarList, initRepoIssueLabelFilter,
} from './features/repo-issue.ts';
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
import {initRepoTopicBar} from './features/repo-home.ts';
@@ -181,6 +181,7 @@ onDomReady(() => {
initRepoGraphGit,
initRepoIssueContentHistory,
initRepoIssueList,
+ initRepoIssueLabelFilter,
initRepoIssueSidebarList,
initRepoIssueReferenceRepositorySearch,
initRepoIssueWipTitle,
diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts
new file mode 100644
index 0000000000..587e0bca7c
--- /dev/null
+++ b/web_src/js/modules/fomantic/dropdown.test.ts
@@ -0,0 +1,56 @@
+import {createElementFromHTML} from '../../utils/dom.ts';
+import {hideScopedEmptyDividers} from './dropdown.ts';
+
+test('hideScopedEmptyDividers-simple', () => {
+ const container = createElementFromHTML(`<div>
+<div class="divider"></div>
+<div class="item">a</div>
+<div class="divider"></div>
+<div class="divider"></div>
+<div class="divider"></div>
+<div class="item">b</div>
+<div class="divider"></div>
+</div>`);
+ hideScopedEmptyDividers(container);
+ expect(container.innerHTML).toEqual(`
+<div class="divider hidden transition"></div>
+<div class="item">a</div>
+<div class="divider hidden transition"></div>
+<div class="divider hidden transition"></div>
+<div class="divider"></div>
+<div class="item">b</div>
+<div class="divider hidden transition"></div>
+`);
+});
+
+test('hideScopedEmptyDividers-hidden1', () => {
+ const container = createElementFromHTML(`<div>
+<div class="item">a</div>
+<div class="divider" data-scope="b"></div>
+<div class="item tw-hidden" data-scope="b">b</div>
+</div>`);
+ hideScopedEmptyDividers(container);
+ expect(container.innerHTML).toEqual(`
+<div class="item">a</div>
+<div class="divider hidden transition" data-scope="b"></div>
+<div class="item tw-hidden" data-scope="b">b</div>
+`);
+});
+
+test('hideScopedEmptyDividers-hidden2', () => {
+ const container = createElementFromHTML(`<div>
+<div class="item" data-scope="">a</div>
+<div class="divider" data-scope="b"></div>
+<div class="item tw-hidden" data-scope="b">b</div>
+<div class="divider" data-scope=""></div>
+<div class="item" data-scope="">c</div>
+</div>`);
+ hideScopedEmptyDividers(container);
+ expect(container.innerHTML).toEqual(`
+<div class="item" data-scope="">a</div>
+<div class="divider hidden transition" data-scope="b"></div>
+<div class="item tw-hidden" data-scope="b">b</div>
+<div class="divider hidden transition" data-scope=""></div>
+<div class="item" data-scope="">c</div>
+`);
+});
diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts
index d8fb4d6e6e..6d0f12cb43 100644
--- a/web_src/js/modules/fomantic/dropdown.ts
+++ b/web_src/js/modules/fomantic/dropdown.ts
@@ -59,6 +59,12 @@ function updateSelectionLabel(label: HTMLElement) {
}
}
+function processMenuItems($dropdown, dropdownCall) {
+ const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty';
+ const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu');
+ if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu);
+}
+
// delegate the dropdown's template functions and callback functions to add aria attributes.
function delegateOne($dropdown: any) {
const dropdownCall = fomanticDropdownFn.bind($dropdown);
@@ -72,6 +78,18 @@ function delegateOne($dropdown: any) {
// * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu
dropdownCall('internal', 'blurSearch', function () { oldBlurSearch.call(this); dropdownCall('hide') });
+ const oldFilterItems = dropdownCall('internal', 'filterItems');
+ dropdownCall('internal', 'filterItems', function (...args: any[]) {
+ oldFilterItems.call(this, ...args);
+ processMenuItems($dropdown, dropdownCall);
+ });
+
+ const oldShow = dropdownCall('internal', 'show');
+ dropdownCall('internal', 'show', function (...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;
@@ -271,3 +289,65 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT
ignoreClickPreEvents = ignoreClickPreVisible = 0;
}, true);
}
+
+// Although Fomantic Dropdown supports "hideDividers", it doesn't really work with our "scoped dividers"
+// At the moment, "label dropdown items" use scopes, a sample case is:
+// * a-label
+// * divider
+// * scope/1
+// * scope/2
+// * divider
+// * z-label
+// when the "scope/*" are filtered out, we'd like to see "a-label" and "z-label" without the divider.
+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 hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items
+
+ const handleScopeSwitch = (itemScope: string) => {
+ if (curScopeVisibleItems.length === 1 && isScopedDivider(curScopeVisibleItems[0])) {
+ hideDivider(curScopeVisibleItems[0]);
+ } else if (curScopeVisibleItems.length) {
+ if (isScopedDivider(curScopeVisibleItems[0]) && lastVisibleScope === curScope) {
+ hideDivider(curScopeVisibleItems[0]);
+ curScopeVisibleItems.shift();
+ }
+ visibleItems.push(...curScopeVisibleItems);
+ lastVisibleScope = curScope;
+ }
+ curScope = itemScope;
+ curScopeVisibleItems.length = 0;
+ };
+
+ // 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')) {
+ curScopeVisibleItems.push(item as HTMLElement);
+ }
+ }
+ handleScopeSwitch('');
+
+ // hide all leading and trailing dividers
+ while (visibleItems.length) {
+ if (!visibleItems[0].matches('.divider')) break;
+ hideDivider(visibleItems[0]);
+ visibleItems.shift();
+ }
+ while (visibleItems.length) {
+ if (!visibleItems[visibleItems.length - 1].matches('.divider')) 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);
+ }
+}
diff --git a/web_src/js/webcomponents/overflow-menu.ts b/web_src/js/webcomponents/overflow-menu.ts
index 777d7dc65d..4e729a268a 100644
--- a/web_src/js/webcomponents/overflow-menu.ts
+++ b/web_src/js/webcomponents/overflow-menu.ts
@@ -12,7 +12,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
mutationObserver: MutationObserver;
lastWidth: number;
- updateItems = throttle(100, () => { // eslint-disable-line unicorn/consistent-function-scoping -- https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2088
+ updateItems = throttle(100, () => {
if (!this.tippyContent) {
const div = document.createElement('div');
div.classList.add('tippy-target');