|
| 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]')) params.append('files[]', fileInput.value); |
| 129 | + |
| 130 | + const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params}); |
| 131 | + const data = await response.json(); |
| 132 | + if (!data.content) { |
| 133 | + renderContent.innerHTML = document.getElementById('no-content').innerHTML; |
| 134 | + rawContent.textContent = ''; |
| 135 | + } else { |
| 136 | + renderContent.innerHTML = data.content; |
| 137 | + rawContent.textContent = comboMarkdownEditor.value(); |
| 138 | + const refIssues = renderContent.querySelectorAll('p .ref-issue'); |
| 139 | + attachRefIssueContextPopup(refIssues); |
| 140 | + } |
| 141 | + const content = segment; |
| 142 | + if (!content.querySelector('.dropzone-attachments')) { |
| 143 | + if (data.attachments !== '') { |
| 144 | + content.insertAdjacentHTML('beforeend', data.attachments); |
| 145 | + } |
| 146 | + } else if (data.attachments === '') { |
| 147 | + content.querySelector('.dropzone-attachments').remove(); |
| 148 | + } else { |
| 149 | + content.querySelector('.dropzone-attachments').outerHTML = data.attachments; |
| 150 | + } |
| 151 | + dropzoneInst?.emit('submit'); |
| 152 | + dropzoneInst?.emit('reload'); |
| 153 | + initMarkupContent(); |
| 154 | + initCommentContent(); |
| 155 | + } catch (error) { |
| 156 | + console.error(error); |
| 157 | + } |
| 158 | + }; |
| 159 | + |
| 160 | + comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); |
| 161 | + if (!comboMarkdownEditor) { |
| 162 | + editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML; |
| 163 | + comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); |
| 164 | + comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone')); |
| 165 | + editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset); |
| 166 | + editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh); |
| 167 | + } |
| 168 | + |
| 169 | + // Show write/preview tab and copy raw content as needed |
| 170 | + showElem(editContentZone); |
| 171 | + hideElem(renderContent); |
| 172 | + if (!comboMarkdownEditor.value()) { |
| 173 | + comboMarkdownEditor.value(rawContent.textContent); |
| 174 | + } |
| 175 | + comboMarkdownEditor.focus(); |
| 176 | +} |
| 177 | + |
| 178 | +export function initRepoIssueCommentEdit() { |
| 179 | + // Edit issue or comment content |
| 180 | + $(document).on('click', '.edit-content', onEditContent); |
| 181 | + |
| 182 | + // Quote reply |
| 183 | + $(document).on('click', '.quote-reply', async function (event) { |
| 184 | + event.preventDefault(); |
| 185 | + const target = $(this).data('target'); |
| 186 | + const quote = $(`#${target}`).text().replace(/\n/g, '\n> '); |
| 187 | + const content = `> ${quote}\n\n`; |
| 188 | + let editor; |
| 189 | + if ($(this).hasClass('quote-reply-diff')) { |
| 190 | + const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply'); |
| 191 | + editor = await handleReply($replyBtn); |
| 192 | + } else { |
| 193 | + // for normal issue/comment page |
| 194 | + editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor')); |
| 195 | + } |
| 196 | + if (editor) { |
| 197 | + if (editor.value()) { |
| 198 | + editor.value(`${editor.value()}\n\n${content}`); |
| 199 | + } else { |
| 200 | + editor.value(content); |
| 201 | + } |
| 202 | + editor.focus(); |
| 203 | + editor.moveCursorToEnd(); |
| 204 | + } |
| 205 | + }); |
| 206 | +} |
0 commit comments