aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js/features/repo-issue-sidebar-combolist.ts
blob: d5416159887fac7d4b5869ca5935c6bf893c413d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {POST} from '../modules/fetch.ts';
import {queryElemChildren, toggleElem} from '../utils/dom.ts';

// if there are draft comments, confirm before reloading, to avoid losing comments
export 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();
}

function collectCheckedValues(elDropdown: HTMLElement) {
  return Array.from(elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
}

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);

  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');
    elComboValue.value = collectCheckedValues(elDropdown).join(',');
  });

  const updateToBackend = async (changedValues) => {
    let changed = false;
    for (const value of initialValues) {
      if (!changedValues.includes(value)) {
        await POST(updateUrl, {data: new URLSearchParams({action: 'detach', id: value})});
        changed = true;
      }
    }
    for (const value of changedValues) {
      if (!initialValues.includes(value)) {
        await POST(updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
        changed = true;
      }
    }
    if (changed) issueSidebarReloadConfirmDraftComment();
  };

  const syncList = (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 listItem = el.cloneNode(true) as HTMLElement;
      listItem.querySelector('svg.octicon-check')?.remove();
      elList.append(listItem);
    }
    const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)'));
    toggleElem(elEmptyTip, !hasItems);
  };

  fomanticQuery(elDropdown).dropdown({
    action: 'nothing', // do not hide the menu if user presses Enter
    fullTextSearch: 'exact',
    async onHide() {
      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
      }
    },
  });
}