diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2024-11-10 16:26:42 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-10 08:26:42 +0000 |
commit | 58c634b8549fb279aec72cecd6a48511803db067 (patch) | |
tree | 15f734f16ac5c4cf3a84301ec33dc968845f412b /web_src | |
parent | b55a31eb6a894feb5508e350ff5e9548b2531bd6 (diff) | |
download | gitea-58c634b8549fb279aec72cecd6a48511803db067.tar.gz gitea-58c634b8549fb279aec72cecd6a48511803db067.zip |
Refactor sidebar label selector (#32460)
Introduce `issueSidebarLabelsData` to handle all sidebar labels related data.
Diffstat (limited to 'web_src')
-rw-r--r-- | web_src/css/repo.css | 8 | ||||
-rw-r--r-- | web_src/css/repo/issue-label.css | 5 | ||||
-rw-r--r-- | web_src/js/features/common-page.ts | 4 | ||||
-rw-r--r-- | web_src/js/features/repo-issue-sidebar-combolist.ts | 46 | ||||
-rw-r--r-- | web_src/js/features/repo-issue-sidebar.md | 27 | ||||
-rw-r--r-- | web_src/js/features/repo-issue-sidebar.ts | 20 | ||||
-rw-r--r-- | web_src/js/features/repo-issue.ts | 15 | ||||
-rw-r--r-- | web_src/js/index.ts | 3 | ||||
-rw-r--r-- | web_src/js/utils/dom.ts | 10 |
9 files changed, 83 insertions, 55 deletions
diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 185a5f6f55..ff8342d29a 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -50,7 +50,7 @@ width: 300px; } -.issue-sidebar-combo .ui.dropdown .item:not(.checked) svg.octicon-check { +.issue-sidebar-combo .ui.dropdown .item:not(.checked) .item-check-mark { visibility: hidden; } /* ideally, we should move these styles to ".ui.dropdown .menu.flex-items-menu > .item ...", could be done later */ @@ -62,6 +62,8 @@ .issue-content-right .dropdown > .menu { max-width: 270px; min-width: 0; + max-height: 500px; + overflow-x: auto; } @media (max-width: 767.98px) { @@ -110,10 +112,6 @@ left: 0; } -.repository .select-label .desc { - padding-left: 23px; -} - /* For the secondary pointing menu, respect its own border-bottom */ /* style reference: https://semantic-ui.com/collections/menu.html#pointing */ .repository .ui.tabs.container .ui.menu:not(.secondary.pointing) { diff --git a/web_src/css/repo/issue-label.css b/web_src/css/repo/issue-label.css index 9b4b144a00..0a25d31da9 100644 --- a/web_src/css/repo/issue-label.css +++ b/web_src/css/repo/issue-label.css @@ -47,6 +47,7 @@ } .archived-label-hint { - float: right; - margin: -12px; + position: absolute; + top: 10px; + right: 5px; } diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts index beec92d152..56c5915b6d 100644 --- a/web_src/js/features/common-page.ts +++ b/web_src/js/features/common-page.ts @@ -32,13 +32,13 @@ export function initGlobalDropdown() { const $uiDropdowns = fomanticQuery('.ui.dropdown'); // do not init "custom" dropdowns, "custom" dropdowns are managed by their own code. - $uiDropdowns.filter(':not(.custom)').dropdown(); + $uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'}); // The "jump" means this dropdown is mainly used for "menu" purpose, // clicking an item will jump to somewhere else or trigger an action/function. // When a dropdown is used for non-refresh actions with tippy, // it must have this "jump" class to hide the tippy when dropdown is closed. - $uiDropdowns.filter('.jump').dropdown({ + $uiDropdowns.filter('.jump').dropdown('setting', { action: 'hide', onShow() { // hide associated tooltip while dropdown is open diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts index d541615988..f408eb43ba 100644 --- a/web_src/js/features/repo-issue-sidebar-combolist.ts +++ b/web_src/js/features/repo-issue-sidebar-combolist.ts @@ -1,6 +1,6 @@ import {fomanticQuery} from '../modules/fomantic/base.ts'; import {POST} from '../modules/fetch.ts'; -import {queryElemChildren, toggleElem} from '../utils/dom.ts'; +import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts'; // if there are draft comments, confirm before reloading, to avoid losing comments export function issueSidebarReloadConfirmDraftComment() { @@ -27,20 +27,37 @@ function collectCheckedValues(elDropdown: HTMLElement) { } export function initIssueSidebarComboList(container: HTMLElement) { - if (!container) return; - const updateUrl = container.getAttribute('data-update-url'); const elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown'); const elList = container.querySelector<HTMLElement>(':scope > .ui.list'); const elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value'); - const initialValues = collectCheckedValues(elDropdown); + let initialValues = collectCheckedValues(elDropdown); elDropdown.addEventListener('click', (e) => { const elItem = (e.target as HTMLElement).closest('.item'); if (!elItem) return; e.preventDefault(); - if (elItem.getAttribute('data-can-change') !== 'true') return; - elItem.classList.toggle('checked'); + if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return; + + if (elItem.matches('.clear-selection')) { + queryElems(elDropdown, '.menu > .item', (el) => el.classList.remove('checked')); + elComboValue.value = ''; + return; + } + + const scope = elItem.getAttribute('data-scope'); + if (scope) { + // scoped items could only be checked one at a time + const elSelected = elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`); + if (elSelected === elItem) { + elItem.classList.toggle('checked'); + } else { + queryElems(elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked')); + elItem.classList.toggle('checked', true); + } + } else { + elItem.classList.toggle('checked'); + } elComboValue.value = collectCheckedValues(elDropdown).join(','); }); @@ -61,29 +78,28 @@ export function initIssueSidebarComboList(container: HTMLElement) { if (changed) issueSidebarReloadConfirmDraftComment(); }; - const syncList = (changedValues) => { + const syncUiList = (changedValues) => { const elEmptyTip = elList.querySelector('.item.empty-list'); queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove()); for (const value of changedValues) { - const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${value}"]`); + const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`); const listItem = el.cloneNode(true) as HTMLElement; - listItem.querySelector('svg.octicon-check')?.remove(); + queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove()); elList.append(listItem); } const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)')); toggleElem(elEmptyTip, !hasItems); }; - fomanticQuery(elDropdown).dropdown({ + fomanticQuery(elDropdown).dropdown('setting', { action: 'nothing', // do not hide the menu if user presses Enter fullTextSearch: 'exact', async onHide() { + // TODO: support "Esc" to cancel the selection. Use partial page loading to avoid losing inputs. const changedValues = collectCheckedValues(elDropdown); - if (updateUrl) { - await updateToBackend(changedValues); // send requests to backend and reload the page - } else { - syncList(changedValues); // only update the list in the sidebar - } + syncUiList(changedValues); + if (updateUrl) await updateToBackend(changedValues); + initialValues = changedValues; }, }); } diff --git a/web_src/js/features/repo-issue-sidebar.md b/web_src/js/features/repo-issue-sidebar.md new file mode 100644 index 0000000000..3022b52d05 --- /dev/null +++ b/web_src/js/features/repo-issue-sidebar.md @@ -0,0 +1,27 @@ +A sidebar combo (dropdown+list) is like this: + +```html +<div class="issue-sidebar-combo" data-update-url="..."> + <input class="combo-value" name="..." type="hidden" value="..."> + <div class="ui dropdown"> + <div class="menu"> + <div class="item clear-selection">clear</div> + <div class="item" data-value="..." data-scope="..."> + <span class="item-check-mark">...</span> + ... + </div> + </div> + </div> + <div class="ui list"> + <span class="item empty-list">no item</span> + <span class="item">...</span> + </div> +</div> +``` + +When the selected items change, the `combo-value` input will be updated. +If there is `data-update-url`, it also calls backend to attach/detach the changed items. + +Also, the changed items will be syncronized to the `ui list` items. + +The items with the same data-scope only allow one selected at a time. diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts index 4a1ef02aab..52878848e8 100644 --- a/web_src/js/features/repo-issue-sidebar.ts +++ b/web_src/js/features/repo-issue-sidebar.ts @@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts'; import {updateIssuesMeta} from './repo-common.ts'; import {svg} from '../svg.ts'; import {htmlEscape} from 'escape-goat'; -import {toggleElem} from '../utils/dom.ts'; +import {queryElems, toggleElem} from '../utils/dom.ts'; import {initIssueSidebarComboList, issueSidebarReloadConfirmDraftComment} from './repo-issue-sidebar-combolist.ts'; function initBranchSelector() { @@ -28,7 +28,7 @@ function initBranchSelector() { } else { // for new issue, only update UI&form, do not send request/reload const selectedHiddenSelector = this.getAttribute('data-id-selector'); - document.querySelector(selectedHiddenSelector).value = selectedValue; + document.querySelector<HTMLInputElement>(selectedHiddenSelector).value = selectedValue; elSelectBranch.querySelector('.text-branch-name').textContent = selectedText; } }); @@ -53,7 +53,7 @@ function initListSubmits(selector, outerSelector) { for (const [elementId, item] of itemEntries) { await updateIssuesMeta( item['update-url'], - item.action, + item['action'], item['issue-id'], elementId, ); @@ -80,14 +80,14 @@ function initListSubmits(selector, outerSelector) { if (scope) { // Enable only clicked item for scoped labels if (this.getAttribute('data-scope') !== scope) { - return true; + return; } if (this !== clickedItem && !this.classList.contains('checked')) { - return true; + return; } } else if (this !== clickedItem) { // Toggle for other labels - return true; + return; } if (this.classList.contains('checked')) { @@ -258,13 +258,13 @@ export function initRepoIssueSidebar() { initRepoIssueDue(); // TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList - initListSubmits('select-label', 'labels'); initListSubmits('select-assignees', 'assignees'); initListSubmits('select-assignees-modify', 'assignees'); + selectItem('.select-assignee', '#assignee_id'); + selectItem('.select-project', '#project_id'); selectItem('.select-milestone', '#milestone_id'); - selectItem('.select-assignee', '#assignee_id'); - // init the combo list: a dropdown for selecting reviewers, and a list for showing selected reviewers and related actions - initIssueSidebarComboList(document.querySelector('.issue-sidebar-combo[data-sidebar-combo-for="reviewers"]')); + // init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions + queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el)); } diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index 92916ec8d7..7457531ece 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -98,6 +98,7 @@ export function initRepoIssueSidebarList() { }); }); + // FIXME: it is wrong place to init ".ui.dropdown.label-filter" $('.menu .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'); @@ -106,7 +107,6 @@ export function initRepoIssueSidebarList() { } } }); - $('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems'); } export function initRepoIssueCommentDelete() { @@ -652,19 +652,6 @@ function initIssueTemplateCommentEditors($commentForm) { } } -// This function used to show and hide archived label on issue/pr -// page in the sidebar where we select the labels -// If we have any archived label tagged to issue and pr. We will show that -// archived label with checked classed otherwise we will hide it -// with the help of this function. -// This function runs globally. -export function initArchivedLabelHandler() { - if (!document.querySelector('.archived-label-hint')) return; - for (const label of document.querySelectorAll('[data-is-archived]')) { - toggleElem(label, label.classList.contains('checked')); - } -} - export function initRepoCommentFormAndSidebar() { const $commentForm = $('.comment.form'); if (!$commentForm.length) return; diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 487aac97aa..eeead37333 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -30,7 +30,7 @@ import { initRepoIssueWipTitle, initRepoPullRequestMergeInstruction, initRepoPullRequestAllowMaintainerEdit, - initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler, + initRepoPullRequestReview, initRepoIssueSidebarList, } from './features/repo-issue.ts'; import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; @@ -182,7 +182,6 @@ onDomReady(() => { initRepoIssueContentHistory, initRepoIssueList, initRepoIssueSidebarList, - initArchivedLabelHandler, initRepoIssueReferenceRepositorySearch, initRepoIssueTimeTracking, initRepoIssueWipTitle, diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 79ce05a7ad..29b34dd1e3 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -3,7 +3,7 @@ import type {Promisable} from 'type-fest'; import type $ from 'jquery'; type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>; -type ElementsCallback = (el: Element) => Promisable<any>; +type ElementsCallback<T extends Element> = (el: T) => Promisable<any>; type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>; type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array @@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) { return res[0]; } -function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback): ArrayLikeIterable<T> { +function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> { if (fn) { for (const el of elems) { fn(el); @@ -67,7 +67,7 @@ function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: return elems; } -export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> { +export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> { const elems = Array.from(el.parentNode.children) as T[]; return applyElemsCallback<T>(elems.filter((child: Element) => { return child !== el && child.matches(selector); @@ -75,13 +75,13 @@ export function queryElemSiblings<T extends Element>(el: Element, selector = '*' } // it works like jQuery.children: only the direct children are selected -export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> { +export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> { return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn); } // it works like parent.querySelectorAll: all descendants are selected // in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent -export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable<T> { +export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> { return applyElemsCallback<T>(parent.querySelectorAll(selector), fn); } |