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 }