mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:45:25 +01:00 
			
		
		
		
	Refactor dropzone (#31482)
Refactor the legacy code and remove some jQuery calls.
This commit is contained in:
		
							parent
							
								
									35ce7a5e0e
								
							
						
					
					
						commit
						a88f718c10
					
				| @ -11,6 +11,7 @@ import {initTextExpander} from './TextExpander.js'; | ||||
| import {showErrorToast} from '../../modules/toast.js'; | ||||
| import {POST} from '../../modules/fetch.js'; | ||||
| import {initTextareaMarkdown} from './EditorMarkdown.js'; | ||||
| import {initDropzone} from '../dropzone.js'; | ||||
| 
 | ||||
| let elementIdCounter = 0; | ||||
| 
 | ||||
| @ -47,7 +48,7 @@ class ComboMarkdownEditor { | ||||
|     this.prepareEasyMDEToolbarActions(); | ||||
|     this.setupContainer(); | ||||
|     this.setupTab(); | ||||
|     this.setupDropzone(); | ||||
|     await this.setupDropzone(); // textarea depends on dropzone
 | ||||
|     this.setupTextarea(); | ||||
| 
 | ||||
|     await this.switchToUserPreference(); | ||||
| @ -114,13 +115,30 @@ class ComboMarkdownEditor { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setupDropzone() { | ||||
|   async setupDropzone() { | ||||
|     const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container'); | ||||
|     if (dropzoneParentContainer) { | ||||
|       this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone'); | ||||
|       if (this.dropzone) this.attachedDropzoneInst = await initDropzone(this.dropzone); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   dropzoneGetFiles() { | ||||
|     if (!this.dropzone) return null; | ||||
|     return Array.from(this.dropzone.querySelectorAll('.files [name=files]'), (el) => el.value); | ||||
|   } | ||||
| 
 | ||||
|   dropzoneReloadFiles() { | ||||
|     if (!this.dropzone) return; | ||||
|     this.attachedDropzoneInst.emit('reload'); | ||||
|   } | ||||
| 
 | ||||
|   dropzoneSubmitReload() { | ||||
|     if (!this.dropzone) return; | ||||
|     this.attachedDropzoneInst.emit('submit'); | ||||
|     this.attachedDropzoneInst.emit('reload'); | ||||
|   } | ||||
| 
 | ||||
|   setupTab() { | ||||
|     const tabs = this.container.querySelectorAll('.tabular.menu > .item'); | ||||
| 
 | ||||
|  | ||||
| @ -1,14 +1,14 @@ | ||||
| import $ from 'jquery'; | ||||
| import {svg} from '../svg.js'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {clippie} from 'clippie'; | ||||
| import {showTemporaryTooltip} from '../modules/tippy.js'; | ||||
| import {POST} from '../modules/fetch.js'; | ||||
| import {GET, POST} from '../modules/fetch.js'; | ||||
| import {showErrorToast} from '../modules/toast.js'; | ||||
| import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.js'; | ||||
| 
 | ||||
| const {csrfToken, i18n} = window.config; | ||||
| 
 | ||||
| export async function createDropzone(el, opts) { | ||||
| async function createDropzone(el, opts) { | ||||
|   const [{Dropzone}] = await Promise.all([ | ||||
|     import(/* webpackChunkName: "dropzone" */'dropzone'), | ||||
|     import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), | ||||
| @ -16,65 +16,119 @@ export async function createDropzone(el, opts) { | ||||
|   return new Dropzone(el, opts); | ||||
| } | ||||
| 
 | ||||
| export function initGlobalDropzone() { | ||||
|   for (const el of document.querySelectorAll('.dropzone')) { | ||||
|     initDropzone(el); | ||||
|   } | ||||
| function addCopyLink(file) { | ||||
|   // Create a "Copy Link" element, to conveniently copy the image or file link as Markdown to the clipboard
 | ||||
|   // The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone
 | ||||
|   const copyLinkEl = createElementFromHTML(` | ||||
| <div class="tw-text-center"> | ||||
|   <a href="#" class="tw-cursor-pointer">${svg('octicon-copy', 14)} Copy link</a> | ||||
| </div>`); | ||||
|   copyLinkEl.addEventListener('click', async (e) => { | ||||
|     e.preventDefault(); | ||||
|     let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; | ||||
|     if (file.type?.startsWith('image/')) { | ||||
|       fileMarkdown = `!${fileMarkdown}`; | ||||
|     } else if (file.type?.startsWith('video/')) { | ||||
|       fileMarkdown = `<video src="/attachments/${htmlEscape(file.uuid)}" title="${htmlEscape(file.name)}" controls></video>`; | ||||
|     } | ||||
|     const success = await clippie(fileMarkdown); | ||||
|     showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); | ||||
|   }); | ||||
|   file.previewTemplate.append(copyLinkEl); | ||||
| } | ||||
| 
 | ||||
| export function initDropzone(el) { | ||||
|   const $dropzone = $(el); | ||||
|   const _promise = createDropzone(el, { | ||||
|     url: $dropzone.data('upload-url'), | ||||
| /** | ||||
|  * @param {HTMLElement} dropzoneEl | ||||
|  */ | ||||
| export async function initDropzone(dropzoneEl) { | ||||
|   const listAttachmentsUrl = dropzoneEl.closest('[data-attachment-url]')?.getAttribute('data-attachment-url'); | ||||
|   const removeAttachmentUrl = dropzoneEl.getAttribute('data-remove-url'); | ||||
|   const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); | ||||
| 
 | ||||
|   let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
 | ||||
|   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
 | ||||
|   const opts = { | ||||
|     url: dropzoneEl.getAttribute('data-upload-url'), | ||||
|     headers: {'X-Csrf-Token': csrfToken}, | ||||
|     maxFiles: $dropzone.data('max-file'), | ||||
|     maxFilesize: $dropzone.data('max-size'), | ||||
|     acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'), | ||||
|     acceptedFiles: ['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')) ? null : dropzoneEl.getAttribute('data-accepts'), | ||||
|     addRemoveLinks: true, | ||||
|     dictDefaultMessage: $dropzone.data('default-message'), | ||||
|     dictInvalidFileType: $dropzone.data('invalid-input-type'), | ||||
|     dictFileTooBig: $dropzone.data('file-too-big'), | ||||
|     dictRemoveFile: $dropzone.data('remove-file'), | ||||
|     dictDefaultMessage: dropzoneEl.getAttribute('data-default-message'), | ||||
|     dictInvalidFileType: dropzoneEl.getAttribute('data-invalid-input-type'), | ||||
|     dictFileTooBig: dropzoneEl.getAttribute('data-file-too-big'), | ||||
|     dictRemoveFile: dropzoneEl.getAttribute('data-remove-file'), | ||||
|     timeout: 0, | ||||
|     thumbnailMethod: 'contain', | ||||
|     thumbnailWidth: 480, | ||||
|     thumbnailHeight: 480, | ||||
|     init() { | ||||
|       this.on('success', (file, data) => { | ||||
|         file.uuid = data.uuid; | ||||
|         const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid); | ||||
|         $dropzone.find('.files').append($input); | ||||
|         // Create a "Copy Link" element, to conveniently copy the image
 | ||||
|         // or file link as Markdown to the clipboard
 | ||||
|         const copyLinkElement = document.createElement('div'); | ||||
|         copyLinkElement.className = 'tw-text-center'; | ||||
|         // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
 | ||||
|         copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`; | ||||
|         copyLinkElement.addEventListener('click', async (e) => { | ||||
|           e.preventDefault(); | ||||
|           let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; | ||||
|           if (file.type.startsWith('image/')) { | ||||
|             fileMarkdown = `!${fileMarkdown}`; | ||||
|           } else if (file.type.startsWith('video/')) { | ||||
|             fileMarkdown = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`; | ||||
|           } | ||||
|           const success = await clippie(fileMarkdown); | ||||
|           showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); | ||||
|         }); | ||||
|         file.previewTemplate.append(copyLinkElement); | ||||
|       }); | ||||
|       this.on('removedfile', (file) => { | ||||
|         $(`#${file.uuid}`).remove(); | ||||
|         if ($dropzone.data('remove-url')) { | ||||
|           POST($dropzone.data('remove-url'), { | ||||
|             data: new URLSearchParams({file: file.uuid}), | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|       this.on('error', function (file, message) { | ||||
|         showErrorToast(message); | ||||
|         this.removeFile(file); | ||||
|       }); | ||||
|     }, | ||||
|   }; | ||||
|   if (dropzoneEl.hasAttribute('data-max-file')) opts.maxFiles = Number(dropzoneEl.getAttribute('data-max-file')); | ||||
|   if (dropzoneEl.hasAttribute('data-max-size')) opts.maxFilesize = Number(dropzoneEl.getAttribute('data-max-size')); | ||||
| 
 | ||||
|   // there is a bug in dropzone: if a non-image file is uploaded, then it tries to request the file from server by something like:
 | ||||
|   // "http://localhost:3000/owner/repo/issues/[object%20Event]"
 | ||||
|   // the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '<img src="[object Event]">'
 | ||||
|   const dzInst = await createDropzone(dropzoneEl, opts); | ||||
|   dzInst.on('success', (file, data) => { | ||||
|     file.uuid = data.uuid; | ||||
|     fileUuidDict[file.uuid] = {submitted: false}; | ||||
|     const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${data.uuid}`, value: data.uuid}); | ||||
|     dropzoneEl.querySelector('.files').append(input); | ||||
|     addCopyLink(file); | ||||
|   }); | ||||
| 
 | ||||
|   dzInst.on('removedfile', async (file) => { | ||||
|     if (disableRemovedfileEvent) return; | ||||
|     document.querySelector(`#dropzone-file-${file.uuid}`)?.remove(); | ||||
|     // when the uploaded file number reaches the limit, there is no uuid in the dict, and it doesn't need to be removed from server
 | ||||
|     if (removeAttachmentUrl && fileUuidDict[file.uuid] && !fileUuidDict[file.uuid].submitted) { | ||||
|       await POST(removeAttachmentUrl, {data: new URLSearchParams({file: file.uuid})}); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   dzInst.on('submit', () => { | ||||
|     for (const fileUuid of Object.keys(fileUuidDict)) { | ||||
|       fileUuidDict[fileUuid].submitted = true; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   dzInst.on('reload', async () => { | ||||
|     try { | ||||
|       const resp = await GET(listAttachmentsUrl); | ||||
|       const respData = await resp.json(); | ||||
|       // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
 | ||||
|       disableRemovedfileEvent = true; | ||||
|       dzInst.removeAllFiles(true); | ||||
|       disableRemovedfileEvent = false; | ||||
| 
 | ||||
|       dropzoneEl.querySelector('.files').innerHTML = ''; | ||||
|       for (const el of dropzoneEl.querySelectorAll('.dz-preview')) el.remove(); | ||||
|       fileUuidDict = {}; | ||||
|       for (const attachment of respData) { | ||||
|         const imgSrc = `${attachmentBaseLinkUrl}/${attachment.uuid}`; | ||||
|         dzInst.emit('addedfile', attachment); | ||||
|         dzInst.emit('thumbnail', attachment, imgSrc); | ||||
|         dzInst.emit('complete', attachment); | ||||
|         addCopyLink(attachment); | ||||
|         fileUuidDict[attachment.uuid] = {submitted: true}; | ||||
|         const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${attachment.uuid}`, value: attachment.uuid}); | ||||
|         dropzoneEl.querySelector('.files').append(input); | ||||
|       } | ||||
|       if (!dropzoneEl.querySelector('.dz-preview')) { | ||||
|         dropzoneEl.classList.remove('dz-started'); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       // TODO: if listing the existing attachments failed, it should stop from operating the content or attachments,
 | ||||
|       //  otherwise the attachments might be lost.
 | ||||
|       showErrorToast(`Failed to load attachments: ${error}`); | ||||
|       console.error(error); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   dzInst.on('error', (file, message) => { | ||||
|     showErrorToast(`Dropzone upload error: ${message}`); | ||||
|     dzInst.removeFile(file); | ||||
|   }); | ||||
| 
 | ||||
|   if (listAttachmentsUrl) dzInst.emit('reload'); | ||||
|   return dzInst; | ||||
| } | ||||
|  | ||||
| @ -5,6 +5,7 @@ import {hideElem, queryElems, showElem} from '../utils/dom.js'; | ||||
| import {initMarkupContent} from '../markup/content.js'; | ||||
| import {attachRefIssueContextPopup} from './contextpopup.js'; | ||||
| import {POST} from '../modules/fetch.js'; | ||||
| import {initDropzone} from './dropzone.js'; | ||||
| 
 | ||||
| function initEditPreviewTab($form) { | ||||
|   const $tabMenu = $form.find('.repo-editor-menu'); | ||||
| @ -41,8 +42,11 @@ function initEditPreviewTab($form) { | ||||
| } | ||||
| 
 | ||||
| export function initRepoEditor() { | ||||
|   const $editArea = $('.repository.editor textarea#edit_area'); | ||||
|   if (!$editArea.length) return; | ||||
|   const dropzoneUpload = document.querySelector('.page-content.repository.editor.upload .dropzone'); | ||||
|   if (dropzoneUpload) initDropzone(dropzoneUpload); | ||||
| 
 | ||||
|   const editArea = document.querySelector('.page-content.repository.editor textarea#edit_area'); | ||||
|   if (!editArea) return; | ||||
| 
 | ||||
|   for (const el of queryElems('.js-quick-pull-choice-option')) { | ||||
|     el.addEventListener('input', () => { | ||||
| @ -108,7 +112,7 @@ export function initRepoEditor() { | ||||
|   initEditPreviewTab($form); | ||||
| 
 | ||||
|   (async () => { | ||||
|     const editor = await createCodeEditor($editArea[0], filenameInput); | ||||
|     const editor = await createCodeEditor(editArea, filenameInput); | ||||
| 
 | ||||
|     // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
 | ||||
|     // to enable or disable the commit button
 | ||||
| @ -142,7 +146,7 @@ export function initRepoEditor() { | ||||
| 
 | ||||
|     commitButton?.addEventListener('click', (e) => { | ||||
|       // A modal which asks if an empty file should be committed
 | ||||
|       if (!$editArea.val()) { | ||||
|       if (!editArea.value) { | ||||
|         $('#edit-empty-content-modal').modal({ | ||||
|           onApprove() { | ||||
|             $('.edit.form').trigger('submit'); | ||||
|  | ||||
| @ -1,15 +1,12 @@ | ||||
| import $ from 'jquery'; | ||||
| import {handleReply} from './repo-issue.js'; | ||||
| import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; | ||||
| import {createDropzone} from './dropzone.js'; | ||||
| import {GET, POST} from '../modules/fetch.js'; | ||||
| import {POST} from '../modules/fetch.js'; | ||||
| import {showErrorToast} from '../modules/toast.js'; | ||||
| import {hideElem, showElem} from '../utils/dom.js'; | ||||
| import {attachRefIssueContextPopup} from './contextpopup.js'; | ||||
| import {initCommentContent, initMarkupContent} from '../markup/content.js'; | ||||
| 
 | ||||
| const {csrfToken} = window.config; | ||||
| 
 | ||||
| async function onEditContent(event) { | ||||
|   event.preventDefault(); | ||||
| 
 | ||||
| @ -20,114 +17,27 @@ async function onEditContent(event) { | ||||
| 
 | ||||
|   let comboMarkdownEditor; | ||||
| 
 | ||||
|   /** | ||||
|    * @param {HTMLElement} dropzone | ||||
|    */ | ||||
|   const setupDropzone = async (dropzone) => { | ||||
|     if (!dropzone) return null; | ||||
| 
 | ||||
|     let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
 | ||||
|     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
 | ||||
|     const dz = await createDropzone(dropzone, { | ||||
|       url: dropzone.getAttribute('data-upload-url'), | ||||
|       headers: {'X-Csrf-Token': csrfToken}, | ||||
|       maxFiles: dropzone.getAttribute('data-max-file'), | ||||
|       maxFilesize: dropzone.getAttribute('data-max-size'), | ||||
|       acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'), | ||||
|       addRemoveLinks: true, | ||||
|       dictDefaultMessage: dropzone.getAttribute('data-default-message'), | ||||
|       dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'), | ||||
|       dictFileTooBig: dropzone.getAttribute('data-file-too-big'), | ||||
|       dictRemoveFile: dropzone.getAttribute('data-remove-file'), | ||||
|       timeout: 0, | ||||
|       thumbnailMethod: 'contain', | ||||
|       thumbnailWidth: 480, | ||||
|       thumbnailHeight: 480, | ||||
|       init() { | ||||
|         this.on('success', (file, data) => { | ||||
|           file.uuid = data.uuid; | ||||
|           fileUuidDict[file.uuid] = {submitted: false}; | ||||
|           const input = document.createElement('input'); | ||||
|           input.id = data.uuid; | ||||
|           input.name = 'files'; | ||||
|           input.type = 'hidden'; | ||||
|           input.value = data.uuid; | ||||
|           dropzone.querySelector('.files').append(input); | ||||
|         }); | ||||
|         this.on('removedfile', async (file) => { | ||||
|           document.querySelector(`#${file.uuid}`)?.remove(); | ||||
|           if (disableRemovedfileEvent) return; | ||||
|           if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) { | ||||
|             try { | ||||
|               await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})}); | ||||
|             } catch (error) { | ||||
|               console.error(error); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|         this.on('submit', () => { | ||||
|           for (const fileUuid of Object.keys(fileUuidDict)) { | ||||
|             fileUuidDict[fileUuid].submitted = true; | ||||
|           } | ||||
|         }); | ||||
|         this.on('reload', async () => { | ||||
|           try { | ||||
|             const response = await GET(editContentZone.getAttribute('data-attachment-url')); | ||||
|             const data = await response.json(); | ||||
|             // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
 | ||||
|             disableRemovedfileEvent = true; | ||||
|             dz.removeAllFiles(true); | ||||
|             dropzone.querySelector('.files').innerHTML = ''; | ||||
|             for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove(); | ||||
|             fileUuidDict = {}; | ||||
|             disableRemovedfileEvent = false; | ||||
| 
 | ||||
|             for (const attachment of data) { | ||||
|               const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`; | ||||
|               dz.emit('addedfile', attachment); | ||||
|               dz.emit('thumbnail', attachment, imgSrc); | ||||
|               dz.emit('complete', attachment); | ||||
|               fileUuidDict[attachment.uuid] = {submitted: true}; | ||||
|               dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%'; | ||||
|               const input = document.createElement('input'); | ||||
|               input.id = attachment.uuid; | ||||
|               input.name = 'files'; | ||||
|               input.type = 'hidden'; | ||||
|               input.value = attachment.uuid; | ||||
|               dropzone.querySelector('.files').append(input); | ||||
|             } | ||||
|             if (!dropzone.querySelector('.dz-preview')) { | ||||
|               dropzone.classList.remove('dz-started'); | ||||
|             } | ||||
|           } catch (error) { | ||||
|             console.error(error); | ||||
|           } | ||||
|         }); | ||||
|       }, | ||||
|     }); | ||||
|     dz.emit('reload'); | ||||
|     return dz; | ||||
|   }; | ||||
| 
 | ||||
|   const cancelAndReset = (e) => { | ||||
|     e.preventDefault(); | ||||
|     showElem(renderContent); | ||||
|     hideElem(editContentZone); | ||||
|     comboMarkdownEditor.attachedDropzoneInst?.emit('reload'); | ||||
|     comboMarkdownEditor.dropzoneReloadFiles(); | ||||
|   }; | ||||
| 
 | ||||
|   const saveAndRefresh = async (e) => { | ||||
|     e.preventDefault(); | ||||
|     renderContent.classList.add('is-loading'); | ||||
|     showElem(renderContent); | ||||
|     hideElem(editContentZone); | ||||
|     const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst; | ||||
|     try { | ||||
|       const params = new URLSearchParams({ | ||||
|         content: comboMarkdownEditor.value(), | ||||
|         context: editContentZone.getAttribute('data-context'), | ||||
|         content_version: editContentZone.getAttribute('data-content-version'), | ||||
|       }); | ||||
|       for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value); | ||||
|       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(); | ||||
| @ -155,12 +65,14 @@ async function onEditContent(event) { | ||||
|       } else { | ||||
|         content.querySelector('.dropzone-attachments').outerHTML = data.attachments; | ||||
|       } | ||||
|       dropzoneInst?.emit('submit'); | ||||
|       dropzoneInst?.emit('reload'); | ||||
|       comboMarkdownEditor.dropzoneSubmitReload(); | ||||
|       initMarkupContent(); | ||||
|       initCommentContent(); | ||||
|     } catch (error) { | ||||
|       showErrorToast(`Failed to save the content: ${error}`); | ||||
|       console.error(error); | ||||
|     } finally { | ||||
|       renderContent.classList.remove('is-loading'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
| @ -168,7 +80,6 @@ async function onEditContent(event) { | ||||
|   if (!comboMarkdownEditor) { | ||||
|     editContentZone.innerHTML = document.querySelector('#issue-comment-editor-template').innerHTML; | ||||
|     comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); | ||||
|     comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone')); | ||||
|     editContentZone.querySelector('.ui.cancel.button').addEventListener('click', cancelAndReset); | ||||
|     editContentZone.querySelector('.ui.primary.button').addEventListener('click', saveAndRefresh); | ||||
|   } | ||||
| @ -176,6 +87,7 @@ async function onEditContent(event) { | ||||
|   // Show write/preview tab and copy raw content as needed
 | ||||
|   showElem(editContentZone); | ||||
|   hideElem(renderContent); | ||||
|   // 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); | ||||
|   } | ||||
| @ -196,8 +108,8 @@ export function initRepoIssueCommentEdit() { | ||||
| 
 | ||||
|     let editor; | ||||
|     if (this.classList.contains('quote-reply-diff')) { | ||||
|       const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply'); | ||||
|       editor = await handleReply($replyBtn); | ||||
|       const replyBtn = this.closest('.comment-code-cloud').querySelector('button.comment-form-reply'); | ||||
|       editor = await handleReply(replyBtn); | ||||
|     } else { | ||||
|       // for normal issue/comment page
 | ||||
|       editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor')); | ||||
|  | ||||
| @ -5,7 +5,6 @@ import {hideElem, showElem, toggleElem} from '../utils/dom.js'; | ||||
| import {setFileFolding} from './file-fold.js'; | ||||
| import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; | ||||
| import {toAbsoluteUrl} from '../utils.js'; | ||||
| import {initDropzone} from './dropzone.js'; | ||||
| import {GET, POST} from '../modules/fetch.js'; | ||||
| import {showErrorToast} from '../modules/toast.js'; | ||||
| 
 | ||||
| @ -410,21 +409,13 @@ export function initRepoIssueComments() { | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export async function handleReply($el) { | ||||
|   hideElem($el); | ||||
|   const $form = $el.closest('.comment-code-cloud').find('.comment-form'); | ||||
|   showElem($form); | ||||
| export async function handleReply(el) { | ||||
|   const form = el.closest('.comment-code-cloud').querySelector('.comment-form'); | ||||
|   const textarea = form.querySelector('textarea'); | ||||
| 
 | ||||
|   const $textarea = $form.find('textarea'); | ||||
|   let editor = getComboMarkdownEditor($textarea); | ||||
|   if (!editor) { | ||||
|     // FIXME: the initialization of the dropzone is not consistent.
 | ||||
|     // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
 | ||||
|     // When the form is submitted and partially reload, none of them is initialized.
 | ||||
|     const dropzone = $form.find('.dropzone')[0]; | ||||
|     if (!dropzone.dropzone) initDropzone(dropzone); | ||||
|     editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor')); | ||||
|   } | ||||
|   hideElem(el); | ||||
|   showElem(form); | ||||
|   const editor = getComboMarkdownEditor(textarea) ?? await initComboMarkdownEditor(form.querySelector('.combo-markdown-editor')); | ||||
|   editor.focus(); | ||||
|   return editor; | ||||
| } | ||||
| @ -486,7 +477,7 @@ export function initRepoPullRequestReview() { | ||||
| 
 | ||||
|   $(document).on('click', 'button.comment-form-reply', async function (e) { | ||||
|     e.preventDefault(); | ||||
|     await handleReply($(this)); | ||||
|     await handleReply(this); | ||||
|   }); | ||||
| 
 | ||||
|   const $reviewBox = $('.review-box-panel'); | ||||
| @ -554,8 +545,6 @@ export function initRepoPullRequestReview() { | ||||
|         $td.find("input[name='line']").val(idx); | ||||
|         $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed'); | ||||
|         $td.find("input[name='path']").val(path); | ||||
| 
 | ||||
|         initDropzone($td.find('.dropzone')[0]); | ||||
|         const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor')); | ||||
|         editor.focus(); | ||||
|       } catch (error) { | ||||
|  | ||||
| @ -91,7 +91,6 @@ import { | ||||
|   initGlobalDeleteButton, | ||||
|   initGlobalShowModal, | ||||
| } from './features/common-button.js'; | ||||
| import {initGlobalDropzone} from './features/dropzone.js'; | ||||
| import {initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.js'; | ||||
| 
 | ||||
| initGiteaFomantic(); | ||||
| @ -135,7 +134,6 @@ onDomReady(() => { | ||||
|     initGlobalButtonClickOnEnter, | ||||
|     initGlobalButtons, | ||||
|     initGlobalCopyToClipboardListener, | ||||
|     initGlobalDropzone, | ||||
|     initGlobalEnterQuickSubmit, | ||||
|     initGlobalFormDirtyLeaveConfirm, | ||||
|     initGlobalDeleteButton, | ||||
|  | ||||
| @ -304,3 +304,17 @@ export function createElementFromHTML(htmlString) { | ||||
|   div.innerHTML = htmlString.trim(); | ||||
|   return div.firstChild; | ||||
| } | ||||
| 
 | ||||
| export function createElementFromAttrs(tagName, attrs) { | ||||
|   const el = document.createElement(tagName); | ||||
|   for (const [key, value] of Object.entries(attrs)) { | ||||
|     if (value === undefined || value === null) continue; | ||||
|     if (value === true) { | ||||
|       el.toggleAttribute(key, value); | ||||
|     } else { | ||||
|       el.setAttribute(key, String(value)); | ||||
|     } | ||||
|     // TODO: in the future we could make it also support "textContent" and "innerHTML" properties if needed
 | ||||
|   } | ||||
|   return el; | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,16 @@ | ||||
| import {createElementFromHTML} from './dom.js'; | ||||
| import {createElementFromAttrs, createElementFromHTML} from './dom.js'; | ||||
| 
 | ||||
| test('createElementFromHTML', () => { | ||||
|   expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>'); | ||||
| }); | ||||
| 
 | ||||
| test('createElementFromAttrs', () => { | ||||
|   const el = createElementFromAttrs('button', { | ||||
|     id: 'the-id', | ||||
|     class: 'cls-1 cls-2', | ||||
|     'data-foo': 'the-data', | ||||
|     disabled: true, | ||||
|     required: null, | ||||
|   }); | ||||
|   expect(el.outerHTML).toEqual('<button id="the-id" class="cls-1 cls-2" data-foo="the-data" disabled=""></button>'); | ||||
| }); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user