| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- import {fomanticQuery} from '../modules/fomantic/base.ts';
- import {POST} from '../modules/fetch.ts';
- import {addDelegatedEventListener, 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();
- }
-
- export class IssueSidebarComboList {
- updateUrl: string;
- updateAlgo: string;
- selectionMode: string;
- elDropdown: HTMLElement;
- elList: HTMLElement;
- elComboValue: HTMLInputElement;
- initialValues: string[];
- container: HTMLElement;
-
- constructor(container: HTMLElement) {
- this.container = container;
- this.updateUrl = 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: Array<string>) {
- 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: Array<string>) {
- 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(elItem: HTMLElement, e: Event) {
- 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();
-
- addDelegatedEventListener(this.elDropdown, 'click', '.item', (el, e) => this.onItemClick(el, e));
-
- fomanticQuery(this.elDropdown).dropdown('setting', {
- action: 'nothing', // do not hide the menu if user presses Enter
- fullTextSearch: 'exact',
- hideDividers: 'empty',
- onHide: () => this.onHide(),
- });
- }
- }
|