gitea源码

repo-issue-sidebar-combolist.ts 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import {fomanticQuery} from '../modules/fomantic/base.ts';
  2. import {POST} from '../modules/fetch.ts';
  3. import {addDelegatedEventListener, queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
  4. // if there are draft comments, confirm before reloading, to avoid losing comments
  5. function issueSidebarReloadConfirmDraftComment() {
  6. const commentTextareas = [
  7. document.querySelector<HTMLTextAreaElement>('.edit-content-zone:not(.tw-hidden) textarea'),
  8. document.querySelector<HTMLTextAreaElement>('#comment-form textarea'),
  9. ];
  10. for (const textarea of commentTextareas) {
  11. // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
  12. // But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
  13. if (textarea && textarea.value.trim().length > 10) {
  14. textarea.parentElement.scrollIntoView();
  15. if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
  16. return;
  17. }
  18. break;
  19. }
  20. }
  21. window.location.reload();
  22. }
  23. export class IssueSidebarComboList {
  24. updateUrl: string;
  25. updateAlgo: string;
  26. selectionMode: string;
  27. elDropdown: HTMLElement;
  28. elList: HTMLElement;
  29. elComboValue: HTMLInputElement;
  30. initialValues: string[];
  31. container: HTMLElement;
  32. constructor(container: HTMLElement) {
  33. this.container = container;
  34. this.updateUrl = container.getAttribute('data-update-url');
  35. this.updateAlgo = container.getAttribute('data-update-algo');
  36. this.selectionMode = container.getAttribute('data-selection-mode');
  37. if (!['single', 'multiple'].includes(this.selectionMode)) throw new Error(`Invalid data-update-on: ${this.selectionMode}`);
  38. if (!['diff', 'all'].includes(this.updateAlgo)) throw new Error(`Invalid data-update-algo: ${this.updateAlgo}`);
  39. this.elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
  40. this.elList = container.querySelector<HTMLElement>(':scope > .ui.list');
  41. this.elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
  42. }
  43. collectCheckedValues() {
  44. return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
  45. }
  46. updateUiList(changedValues: Array<string>) {
  47. const elEmptyTip = this.elList.querySelector('.item.empty-list');
  48. queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove());
  49. for (const value of changedValues) {
  50. const el = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
  51. if (!el) continue;
  52. const listItem = el.cloneNode(true) as HTMLElement;
  53. queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
  54. this.elList.append(listItem);
  55. }
  56. const hasItems = Boolean(this.elList.querySelector('.item:not(.empty-list)'));
  57. toggleElem(elEmptyTip, !hasItems);
  58. }
  59. async updateToBackend(changedValues: Array<string>) {
  60. if (this.updateAlgo === 'diff') {
  61. for (const value of this.initialValues) {
  62. if (!changedValues.includes(value)) {
  63. await POST(this.updateUrl, {data: new URLSearchParams({action: 'detach', id: value})});
  64. }
  65. }
  66. for (const value of changedValues) {
  67. if (!this.initialValues.includes(value)) {
  68. await POST(this.updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
  69. }
  70. }
  71. } else {
  72. await POST(this.updateUrl, {data: new URLSearchParams({id: changedValues.join(',')})});
  73. }
  74. issueSidebarReloadConfirmDraftComment();
  75. }
  76. async doUpdate() {
  77. const changedValues = this.collectCheckedValues();
  78. if (this.initialValues.join(',') === changedValues.join(',')) return;
  79. this.updateUiList(changedValues);
  80. if (this.updateUrl) await this.updateToBackend(changedValues);
  81. this.initialValues = changedValues;
  82. }
  83. async onChange() {
  84. if (this.selectionMode === 'single') {
  85. await this.doUpdate();
  86. fomanticQuery(this.elDropdown).dropdown('hide');
  87. }
  88. }
  89. async onItemClick(elItem: HTMLElement, e: Event) {
  90. e.preventDefault();
  91. if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
  92. if (elItem.matches('.clear-selection')) {
  93. queryElems(this.elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
  94. this.elComboValue.value = '';
  95. this.onChange();
  96. return;
  97. }
  98. const scope = elItem.getAttribute('data-scope');
  99. if (scope) {
  100. // scoped items could only be checked one at a time
  101. const elSelected = this.elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
  102. if (elSelected === elItem) {
  103. elItem.classList.toggle('checked');
  104. } else {
  105. queryElems(this.elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
  106. elItem.classList.toggle('checked', true);
  107. }
  108. } else {
  109. if (this.selectionMode === 'multiple') {
  110. elItem.classList.toggle('checked');
  111. } else {
  112. queryElems(this.elDropdown, `.menu > .item.checked`, (el) => el.classList.remove('checked'));
  113. elItem.classList.toggle('checked', true);
  114. }
  115. }
  116. this.elComboValue.value = this.collectCheckedValues().join(',');
  117. this.onChange();
  118. }
  119. async onHide() {
  120. if (this.selectionMode === 'multiple') this.doUpdate();
  121. }
  122. init() {
  123. // init the checked items from initial value
  124. if (this.elComboValue.value && this.elComboValue.value !== '0' && !queryElems(this.elDropdown, `.menu > .item.checked`).length) {
  125. const values = this.elComboValue.value.split(',');
  126. for (const value of values) {
  127. const elItem = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
  128. elItem?.classList.add('checked');
  129. }
  130. this.updateUiList(values);
  131. }
  132. this.initialValues = this.collectCheckedValues();
  133. addDelegatedEventListener(this.elDropdown, 'click', '.item', (el, e) => this.onItemClick(el, e));
  134. fomanticQuery(this.elDropdown).dropdown('setting', {
  135. action: 'nothing', // do not hide the menu if user presses Enter
  136. fullTextSearch: 'exact',
  137. hideDividers: 'empty',
  138. onHide: () => this.onHide(),
  139. });
  140. }
  141. }