mirror of
https://github.com/go-gitea/gitea
synced 2024-11-15 06:34:25 +00:00
a928739456
Follow #32460 Now the code could be much clearer than before and easier to maintain. A lot of legacy code is removed. Manually tested. This PR is large enough, that fine tunes could be deferred to the future if there is no bug found or design problem. Screenshots: <details> ![image](https://github.com/user-attachments/assets/35f4ab7b-1bc0-4bad-a73c-a4569328303c) </details>
160 lines
6.3 KiB
TypeScript
160 lines
6.3 KiB
TypeScript
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
|
import {POST} from '../modules/fetch.ts';
|
|
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
|
|
|
|
// if there are draft comments, confirm before reloading, to avoid losing comments
|
|
function issueSidebarReloadConfirmDraftComment() {
|
|
const commentTextareas = [
|
|
document.querySelector<HTMLTextAreaElement>('.edit-content-zone:not(.tw-hidden) textarea'),
|
|
document.querySelector<HTMLTextAreaElement>('#comment-form textarea'),
|
|
];
|
|
for (const textarea of commentTextareas) {
|
|
// Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
|
|
// But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
|
|
if (textarea && textarea.value.trim().length > 10) {
|
|
textarea.parentElement.scrollIntoView();
|
|
if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
window.location.reload();
|
|
}
|
|
|
|
class IssueSidebarComboList {
|
|
updateUrl: string;
|
|
updateAlgo: string;
|
|
selectionMode: string;
|
|
elDropdown: HTMLElement;
|
|
elList: HTMLElement;
|
|
elComboValue: HTMLInputElement;
|
|
initialValues: string[];
|
|
|
|
constructor(private container: HTMLElement) {
|
|
this.updateUrl = this.container.getAttribute('data-update-url');
|
|
this.updateAlgo = container.getAttribute('data-update-algo');
|
|
this.selectionMode = container.getAttribute('data-selection-mode');
|
|
if (!['single', 'multiple'].includes(this.selectionMode)) throw new Error(`Invalid data-update-on: ${this.selectionMode}`);
|
|
if (!['diff', 'all'].includes(this.updateAlgo)) throw new Error(`Invalid data-update-algo: ${this.updateAlgo}`);
|
|
this.elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
|
|
this.elList = container.querySelector<HTMLElement>(':scope > .ui.list');
|
|
this.elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
|
|
}
|
|
|
|
collectCheckedValues() {
|
|
return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
|
|
}
|
|
|
|
updateUiList(changedValues) {
|
|
const elEmptyTip = this.elList.querySelector('.item.empty-list');
|
|
queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove());
|
|
for (const value of changedValues) {
|
|
const el = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
|
if (!el) continue;
|
|
const listItem = el.cloneNode(true) as HTMLElement;
|
|
queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
|
|
this.elList.append(listItem);
|
|
}
|
|
const hasItems = Boolean(this.elList.querySelector('.item:not(.empty-list)'));
|
|
toggleElem(elEmptyTip, !hasItems);
|
|
}
|
|
|
|
async updateToBackend(changedValues) {
|
|
if (this.updateAlgo === 'diff') {
|
|
for (const value of this.initialValues) {
|
|
if (!changedValues.includes(value)) {
|
|
await POST(this.updateUrl, {data: new URLSearchParams({action: 'detach', id: value})});
|
|
}
|
|
}
|
|
for (const value of changedValues) {
|
|
if (!this.initialValues.includes(value)) {
|
|
await POST(this.updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
|
|
}
|
|
}
|
|
} else {
|
|
await POST(this.updateUrl, {data: new URLSearchParams({id: changedValues.join(',')})});
|
|
}
|
|
issueSidebarReloadConfirmDraftComment();
|
|
}
|
|
|
|
async doUpdate() {
|
|
const changedValues = this.collectCheckedValues();
|
|
if (this.initialValues.join(',') === changedValues.join(',')) return;
|
|
this.updateUiList(changedValues);
|
|
if (this.updateUrl) await this.updateToBackend(changedValues);
|
|
this.initialValues = changedValues;
|
|
}
|
|
|
|
async onChange() {
|
|
if (this.selectionMode === 'single') {
|
|
await this.doUpdate();
|
|
fomanticQuery(this.elDropdown).dropdown('hide');
|
|
}
|
|
}
|
|
|
|
async onItemClick(e) {
|
|
const elItem = (e.target as HTMLElement).closest('.item');
|
|
if (!elItem) return;
|
|
e.preventDefault();
|
|
if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
|
|
|
|
if (elItem.matches('.clear-selection')) {
|
|
queryElems(this.elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
|
|
this.elComboValue.value = '';
|
|
this.onChange();
|
|
return;
|
|
}
|
|
|
|
const scope = elItem.getAttribute('data-scope');
|
|
if (scope) {
|
|
// scoped items could only be checked one at a time
|
|
const elSelected = this.elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
|
|
if (elSelected === elItem) {
|
|
elItem.classList.toggle('checked');
|
|
} else {
|
|
queryElems(this.elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
|
|
elItem.classList.toggle('checked', true);
|
|
}
|
|
} else {
|
|
if (this.selectionMode === 'multiple') {
|
|
elItem.classList.toggle('checked');
|
|
} else {
|
|
queryElems(this.elDropdown, `.menu > .item.checked`, (el) => el.classList.remove('checked'));
|
|
elItem.classList.toggle('checked', true);
|
|
}
|
|
}
|
|
this.elComboValue.value = this.collectCheckedValues().join(',');
|
|
this.onChange();
|
|
}
|
|
|
|
async onHide() {
|
|
if (this.selectionMode === 'multiple') this.doUpdate();
|
|
}
|
|
|
|
init() {
|
|
// init the checked items from initial value
|
|
if (this.elComboValue.value && this.elComboValue.value !== '0' && !queryElems(this.elDropdown, `.menu > .item.checked`).length) {
|
|
const values = this.elComboValue.value.split(',');
|
|
for (const value of values) {
|
|
const elItem = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
|
elItem?.classList.add('checked');
|
|
}
|
|
this.updateUiList(values);
|
|
}
|
|
this.initialValues = this.collectCheckedValues();
|
|
|
|
this.elDropdown.addEventListener('click', (e) => this.onItemClick(e));
|
|
|
|
fomanticQuery(this.elDropdown).dropdown('setting', {
|
|
action: 'nothing', // do not hide the menu if user presses Enter
|
|
fullTextSearch: 'exact',
|
|
onHide: () => this.onHide(),
|
|
});
|
|
}
|
|
}
|
|
|
|
export function initIssueSidebarComboList(container: HTMLElement) {
|
|
new IssueSidebarComboList(container).init();
|
|
}
|