gitea源码

repo-issue-edit.ts 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import {handleReply} from './repo-issue.ts';
  2. import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
  3. import {POST} from '../modules/fetch.ts';
  4. import {showErrorToast} from '../modules/toast.ts';
  5. import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts';
  6. import {attachRefIssueContextPopup} from './contextpopup.ts';
  7. import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
  8. import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
  9. import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts';
  10. async function tryOnEditContent(e: DOMEvent<MouseEvent>) {
  11. const clickTarget = e.target.closest('.edit-content');
  12. if (!clickTarget) return;
  13. e.preventDefault();
  14. const commentContent = clickTarget.closest('.comment-header').nextElementSibling;
  15. const editContentZone = commentContent.querySelector('.edit-content-zone');
  16. let renderContent = commentContent.querySelector('.render-content');
  17. const rawContent = commentContent.querySelector('.raw-content');
  18. let comboMarkdownEditor : ComboMarkdownEditor;
  19. const cancelAndReset = (e: Event) => {
  20. e.preventDefault();
  21. showElem(renderContent);
  22. hideElem(editContentZone);
  23. comboMarkdownEditor.dropzoneReloadFiles();
  24. };
  25. const saveAndRefresh = async (e: Event) => {
  26. e.preventDefault();
  27. // we are already in a form, do not bubble up to the document otherwise there will be other "form submit handlers"
  28. // at the moment, the form submit event conflicts with initRepoDiffConversationForm (global '.conversation-holder form' event handler)
  29. e.stopPropagation();
  30. renderContent.classList.add('is-loading');
  31. showElem(renderContent);
  32. hideElem(editContentZone);
  33. try {
  34. const params = new URLSearchParams({
  35. content: comboMarkdownEditor.value(),
  36. context: editContentZone.getAttribute('data-context'),
  37. content_version: editContentZone.getAttribute('data-content-version'),
  38. });
  39. for (const file of comboMarkdownEditor.dropzoneGetFiles() ?? []) {
  40. params.append('files[]', file);
  41. }
  42. const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
  43. const data = await response.json();
  44. if (!response.ok) {
  45. showErrorToast(data?.errorMessage ?? window.config.i18n.error_occurred);
  46. return;
  47. }
  48. reinitializeAreYouSure(editContentZone.querySelector('form')); // the form is no longer dirty
  49. editContentZone.setAttribute('data-content-version', data.contentVersion);
  50. // replace the render content with new one, to trigger re-initialization of all features
  51. const newRenderContent = renderContent.cloneNode(false) as HTMLElement;
  52. newRenderContent.innerHTML = data.content;
  53. renderContent.replaceWith(newRenderContent);
  54. renderContent = newRenderContent;
  55. rawContent.textContent = comboMarkdownEditor.value();
  56. const refIssues = renderContent.querySelectorAll<HTMLElement>('p .ref-issue');
  57. attachRefIssueContextPopup(refIssues);
  58. if (!commentContent.querySelector('.dropzone-attachments')) {
  59. if (data.attachments !== '') {
  60. commentContent.insertAdjacentHTML('beforeend', data.attachments);
  61. }
  62. } else if (data.attachments === '') {
  63. commentContent.querySelector('.dropzone-attachments').remove();
  64. } else {
  65. commentContent.querySelector('.dropzone-attachments').outerHTML = data.attachments;
  66. }
  67. comboMarkdownEditor.dropzoneSubmitReload();
  68. } catch (error) {
  69. showErrorToast(`Failed to save the content: ${error}`);
  70. console.error(error);
  71. } finally {
  72. renderContent.classList.remove('is-loading');
  73. }
  74. };
  75. // Show write/preview tab and copy raw content as needed
  76. showElem(editContentZone);
  77. hideElem(renderContent);
  78. comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
  79. if (!comboMarkdownEditor) {
  80. editContentZone.innerHTML = document.querySelector('#issue-comment-editor-template').innerHTML;
  81. const form = editContentZone.querySelector('form');
  82. applyAreYouSure(form);
  83. const saveButton = querySingleVisibleElem<HTMLButtonElement>(editContentZone, '.ui.primary.button');
  84. const cancelButton = querySingleVisibleElem<HTMLButtonElement>(editContentZone, '.ui.cancel.button');
  85. comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
  86. const syncUiState = () => saveButton.disabled = comboMarkdownEditor.isUploading();
  87. comboMarkdownEditor.container.addEventListener(ComboMarkdownEditor.EventUploadStateChanged, syncUiState);
  88. cancelButton.addEventListener('click', cancelAndReset);
  89. form.addEventListener('submit', saveAndRefresh);
  90. }
  91. // FIXME: ideally here should reload content and attachment list from backend for existing editor, to avoid losing data
  92. if (!comboMarkdownEditor.value()) {
  93. comboMarkdownEditor.value(rawContent.textContent);
  94. }
  95. comboMarkdownEditor.switchTabToEditor();
  96. comboMarkdownEditor.focus();
  97. triggerUploadStateChanged(comboMarkdownEditor.container);
  98. }
  99. function extractSelectedMarkdown(container: HTMLElement) {
  100. const selection = window.getSelection();
  101. if (!selection.rangeCount) return '';
  102. const range = selection.getRangeAt(0);
  103. if (!container.contains(range.commonAncestorContainer)) return '';
  104. // todo: if commonAncestorContainer parent has "[data-markdown-original-content]" attribute, use the parent's markdown content
  105. // otherwise, use the selected HTML content and respect all "[data-markdown-original-content]/[data-markdown-generated-content]" attributes
  106. const contents = selection.getRangeAt(0).cloneContents();
  107. const el = document.createElement('div');
  108. el.append(contents);
  109. return convertHtmlToMarkdown(el);
  110. }
  111. async function tryOnQuoteReply(e: Event) {
  112. const clickTarget = (e.target as HTMLElement).closest('.quote-reply');
  113. if (!clickTarget) return;
  114. e.preventDefault();
  115. const contentToQuoteId = clickTarget.getAttribute('data-target');
  116. const targetRawToQuote = document.querySelector<HTMLElement>(`#${contentToQuoteId}.raw-content`);
  117. const targetMarkupToQuote = targetRawToQuote.parentElement.querySelector<HTMLElement>('.render-content.markup');
  118. let contentToQuote = extractSelectedMarkdown(targetMarkupToQuote);
  119. if (!contentToQuote) contentToQuote = targetRawToQuote.textContent;
  120. const quotedContent = `${contentToQuote.replace(/^/mg, '> ')}\n\n`;
  121. let editor;
  122. if (clickTarget.classList.contains('quote-reply-diff')) {
  123. const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector<HTMLElement>('button.comment-form-reply');
  124. editor = await handleReply(replyBtn);
  125. } else {
  126. // for normal issue/comment page
  127. editor = getComboMarkdownEditor(document.querySelector('#comment-form .combo-markdown-editor'));
  128. }
  129. if (editor.value()) {
  130. editor.value(`${editor.value()}\n\n${quotedContent}`);
  131. } else {
  132. editor.value(quotedContent);
  133. }
  134. editor.focus();
  135. editor.moveCursorToEnd();
  136. }
  137. export function initRepoIssueCommentEdit() {
  138. document.addEventListener('click', (e) => {
  139. tryOnEditContent(e); // Edit issue or comment content
  140. tryOnQuoteReply(e); // Quote reply to the comment editor
  141. });
  142. }