code.gitea.io/gitea@v1.22.3/web_src/js/features/repo-issue-edit.js (about)

     1  import $ from 'jquery';
     2  import {handleReply} from './repo-issue.js';
     3  import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
     4  import {createDropzone} from './dropzone.js';
     5  import {GET, POST} from '../modules/fetch.js';
     6  import {hideElem, showElem} from '../utils/dom.js';
     7  import {attachRefIssueContextPopup} from './contextpopup.js';
     8  import {initCommentContent, initMarkupContent} from '../markup/content.js';
     9  
    10  const {csrfToken} = window.config;
    11  
    12  async function onEditContent(event) {
    13    event.preventDefault();
    14  
    15    const segment = this.closest('.header').nextElementSibling;
    16    const editContentZone = segment.querySelector('.edit-content-zone');
    17    const renderContent = segment.querySelector('.render-content');
    18    const rawContent = segment.querySelector('.raw-content');
    19  
    20    let comboMarkdownEditor;
    21  
    22    /**
    23     * @param {HTMLElement} dropzone
    24     */
    25    const setupDropzone = async (dropzone) => {
    26      if (!dropzone) return null;
    27  
    28      let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
    29      let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
    30      const dz = await createDropzone(dropzone, {
    31        url: dropzone.getAttribute('data-upload-url'),
    32        headers: {'X-Csrf-Token': csrfToken},
    33        maxFiles: dropzone.getAttribute('data-max-file'),
    34        maxFilesize: dropzone.getAttribute('data-max-size'),
    35        acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'),
    36        addRemoveLinks: true,
    37        dictDefaultMessage: dropzone.getAttribute('data-default-message'),
    38        dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'),
    39        dictFileTooBig: dropzone.getAttribute('data-file-too-big'),
    40        dictRemoveFile: dropzone.getAttribute('data-remove-file'),
    41        timeout: 0,
    42        thumbnailMethod: 'contain',
    43        thumbnailWidth: 480,
    44        thumbnailHeight: 480,
    45        init() {
    46          this.on('success', (file, data) => {
    47            file.uuid = data.uuid;
    48            fileUuidDict[file.uuid] = {submitted: false};
    49            const input = document.createElement('input');
    50            input.id = data.uuid;
    51            input.name = 'files';
    52            input.type = 'hidden';
    53            input.value = data.uuid;
    54            dropzone.querySelector('.files').append(input);
    55          });
    56          this.on('removedfile', async (file) => {
    57            document.getElementById(file.uuid)?.remove();
    58            if (disableRemovedfileEvent) return;
    59            if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
    60              try {
    61                await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
    62              } catch (error) {
    63                console.error(error);
    64              }
    65            }
    66          });
    67          this.on('submit', () => {
    68            for (const fileUuid of Object.keys(fileUuidDict)) {
    69              fileUuidDict[fileUuid].submitted = true;
    70            }
    71          });
    72          this.on('reload', async () => {
    73            try {
    74              const response = await GET(editContentZone.getAttribute('data-attachment-url'));
    75              const data = await response.json();
    76              // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
    77              disableRemovedfileEvent = true;
    78              dz.removeAllFiles(true);
    79              dropzone.querySelector('.files').innerHTML = '';
    80              for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
    81              fileUuidDict = {};
    82              disableRemovedfileEvent = false;
    83  
    84              for (const attachment of data) {
    85                const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
    86                dz.emit('addedfile', attachment);
    87                dz.emit('thumbnail', attachment, imgSrc);
    88                dz.emit('complete', attachment);
    89                fileUuidDict[attachment.uuid] = {submitted: true};
    90                dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
    91                const input = document.createElement('input');
    92                input.id = attachment.uuid;
    93                input.name = 'files';
    94                input.type = 'hidden';
    95                input.value = attachment.uuid;
    96                dropzone.querySelector('.files').append(input);
    97              }
    98              if (!dropzone.querySelector('.dz-preview')) {
    99                dropzone.classList.remove('dz-started');
   100              }
   101            } catch (error) {
   102              console.error(error);
   103            }
   104          });
   105        },
   106      });
   107      dz.emit('reload');
   108      return dz;
   109    };
   110  
   111    const cancelAndReset = (e) => {
   112      e.preventDefault();
   113      showElem(renderContent);
   114      hideElem(editContentZone);
   115      comboMarkdownEditor.attachedDropzoneInst?.emit('reload');
   116    };
   117  
   118    const saveAndRefresh = async (e) => {
   119      e.preventDefault();
   120      showElem(renderContent);
   121      hideElem(editContentZone);
   122      const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst;
   123      try {
   124        const params = new URLSearchParams({
   125          content: comboMarkdownEditor.value(),
   126          context: editContentZone.getAttribute('data-context'),
   127        });
   128        for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]') ?? []) {
   129          params.append('files[]', fileInput.value);
   130        }
   131  
   132        const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
   133        const data = await response.json();
   134        if (!data.content) {
   135          renderContent.innerHTML = document.getElementById('no-content').innerHTML;
   136          rawContent.textContent = '';
   137        } else {
   138          renderContent.innerHTML = data.content;
   139          rawContent.textContent = comboMarkdownEditor.value();
   140          const refIssues = renderContent.querySelectorAll('p .ref-issue');
   141          attachRefIssueContextPopup(refIssues);
   142        }
   143        const content = segment;
   144        if (!content.querySelector('.dropzone-attachments')) {
   145          if (data.attachments !== '') {
   146            content.insertAdjacentHTML('beforeend', data.attachments);
   147          }
   148        } else if (data.attachments === '') {
   149          content.querySelector('.dropzone-attachments').remove();
   150        } else {
   151          content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
   152        }
   153        dropzoneInst?.emit('submit');
   154        dropzoneInst?.emit('reload');
   155        initMarkupContent();
   156        initCommentContent();
   157      } catch (error) {
   158        console.error(error);
   159      }
   160    };
   161  
   162    comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
   163    if (!comboMarkdownEditor) {
   164      editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
   165      comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
   166      comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
   167      editContentZone.querySelector('.ui.cancel.button').addEventListener('click', cancelAndReset);
   168      editContentZone.querySelector('.ui.primary.button').addEventListener('click', saveAndRefresh);
   169    }
   170  
   171    // Show write/preview tab and copy raw content as needed
   172    showElem(editContentZone);
   173    hideElem(renderContent);
   174    if (!comboMarkdownEditor.value()) {
   175      comboMarkdownEditor.value(rawContent.textContent);
   176    }
   177    comboMarkdownEditor.switchTabToEditor();
   178    comboMarkdownEditor.focus();
   179  }
   180  
   181  export function initRepoIssueCommentEdit() {
   182    // Edit issue or comment content
   183    $(document).on('click', '.edit-content', onEditContent);
   184  
   185    // Quote reply
   186    $(document).on('click', '.quote-reply', async function (event) {
   187      event.preventDefault();
   188      const target = $(this).data('target');
   189      const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
   190      const content = `> ${quote}\n\n`;
   191      let editor;
   192      if ($(this).hasClass('quote-reply-diff')) {
   193        const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
   194        editor = await handleReply($replyBtn);
   195      } else {
   196        // for normal issue/comment page
   197        editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
   198      }
   199      if (editor) {
   200        if (editor.value()) {
   201          editor.value(`${editor.value()}\n\n${content}`);
   202        } else {
   203          editor.value(content);
   204        }
   205        editor.focus();
   206        editor.moveCursorToEnd();
   207      }
   208    });
   209  }