aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js/features/repo-issue-edit.ts
blob: 9d146951bd42fbe50b0e40db77f5557693b59947 (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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import {handleReply} from './repo-issue.ts';
import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {hideElem, querySingleVisibleElem, showElem} from '../utils/dom.ts';
import {attachRefIssueContextPopup} from './contextpopup.ts';
import {initCommentContent, initMarkupContent} from '../markup/content.ts';
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';

async function tryOnEditContent(e) {
  const clickTarget = e.target.closest('.edit-content');
  if (!clickTarget) return;

  e.preventDefault();
  const segment = clickTarget.closest('.header').nextElementSibling;
  const editContentZone = segment.querySelector('.edit-content-zone');
  const renderContent = segment.querySelector('.render-content');
  const rawContent = segment.querySelector('.raw-content');

  let comboMarkdownEditor : ComboMarkdownEditor;

  const cancelAndReset = (e) => {
    e.preventDefault();
    showElem(renderContent);
    hideElem(editContentZone);
    comboMarkdownEditor.dropzoneReloadFiles();
  };

  const saveAndRefresh = async (e) => {
    e.preventDefault();
    renderContent.classList.add('is-loading');
    showElem(renderContent);
    hideElem(editContentZone);
    try {
      const params = new URLSearchParams({
        content: comboMarkdownEditor.value(),
        context: editContentZone.getAttribute('data-context'),
        content_version: editContentZone.getAttribute('data-content-version'),
      });
      for (const file of comboMarkdownEditor.dropzoneGetFiles() ?? []) {
        params.append('files[]', file);
      }

      const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
      const data = await response.json();
      if (response.status === 400) {
        showErrorToast(data.errorMessage);
        return;
      }
      editContentZone.setAttribute('data-content-version', data.contentVersion);
      if (!data.content) {
        renderContent.innerHTML = document.querySelector('#no-content').innerHTML;
        rawContent.textContent = '';
      } else {
        renderContent.innerHTML = data.content;
        rawContent.textContent = comboMarkdownEditor.value();
        const refIssues = renderContent.querySelectorAll('p .ref-issue');
        attachRefIssueContextPopup(refIssues);
      }
      const content = segment;
      if (!content.querySelector('.dropzone-attachments')) {
        if (data.attachments !== '') {
          content.insertAdjacentHTML('beforeend', data.attachments);
        }
      } else if (data.attachments === '') {
        content.querySelector('.dropzone-attachments').remove();
      } else {
        content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
      }
      comboMarkdownEditor.dropzoneSubmitReload();
      initMarkupContent();
      initCommentContent();
    } catch (error) {
      showErrorToast(`Failed to save the content: ${error}`);
      console.error(error);
    } finally {
      renderContent.classList.remove('is-loading');
    }
  };

  // Show write/preview tab and copy raw content as needed
  showElem(editContentZone);
  hideElem(renderContent);

  comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
  if (!comboMarkdownEditor) {
    editContentZone.innerHTML = document.querySelector('#issue-comment-editor-template').innerHTML;
    const saveButton = querySingleVisibleElem<HTMLButtonElement>(editContentZone, '.ui.primary.button');
    const cancelButton = querySingleVisibleElem<HTMLButtonElement>(editContentZone, '.ui.cancel.button');
    comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
    const syncUiState = () => saveButton.disabled = comboMarkdownEditor.isUploading();
    comboMarkdownEditor.container.addEventListener(ComboMarkdownEditor.EventUploadStateChanged, syncUiState);
    cancelButton.addEventListener('click', cancelAndReset);
    saveButton.addEventListener('click', saveAndRefresh);
  }

  // FIXME: ideally here should reload content and attachment list from backend for existing editor, to avoid losing data
  if (!comboMarkdownEditor.value()) {
    comboMarkdownEditor.value(rawContent.textContent);
  }
  comboMarkdownEditor.switchTabToEditor();
  comboMarkdownEditor.focus();
  triggerUploadStateChanged(comboMarkdownEditor.container);
}

function extractSelectedMarkdown(container: HTMLElement) {
  const selection = window.getSelection();
  if (!selection.rangeCount) return '';
  const range = selection.getRangeAt(0);
  if (!container.contains(range.commonAncestorContainer)) return '';

  // todo: if commonAncestorContainer parent has "[data-markdown-original-content]" attribute, use the parent's markdown content
  // otherwise, use the selected HTML content and respect all "[data-markdown-original-content]/[data-markdown-generated-content]" attributes
  const contents = selection.getRangeAt(0).cloneContents();
  const el = document.createElement('div');
  el.append(contents);
  return convertHtmlToMarkdown(el);
}

async function tryOnQuoteReply(e) {
  const clickTarget = (e.target as HTMLElement).closest('.quote-reply');
  if (!clickTarget) return;

  e.preventDefault();
  const contentToQuoteId = clickTarget.getAttribute('data-target');
  const targetRawToQuote = document.querySelector<HTMLElement>(`#${contentToQuoteId}.raw-content`);
  const targetMarkupToQuote = targetRawToQuote.parentElement.querySelector<HTMLElement>('.render-content.markup');
  let contentToQuote = extractSelectedMarkdown(targetMarkupToQuote);
  if (!contentToQuote) contentToQuote = targetRawToQuote.textContent;
  const quotedContent = `${contentToQuote.replace(/^/mg, '> ')}\n`;

  let editor;
  if (clickTarget.classList.contains('quote-reply-diff')) {
    const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector('button.comment-form-reply');
    editor = await handleReply(replyBtn);
  } else {
    // for normal issue/comment page
    editor = getComboMarkdownEditor(document.querySelector('#comment-form .combo-markdown-editor'));
  }

  if (editor.value()) {
    editor.value(`${editor.value()}\n\n${quotedContent}`);
  } else {
    editor.value(quotedContent);
  }
  editor.focus();
  editor.moveCursorToEnd();
}

export function initRepoIssueCommentEdit() {
  document.addEventListener('click', (e) => {
    tryOnEditContent(e); // Edit issue or comment content
    tryOnQuoteReply(e); // Quote reply to the comment editor
  });
}