mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Prevent from submitting issue/comment on uploading (#32263)
fix #32262 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
		| @@ -3,14 +3,19 @@ import '@github/text-expander-element'; | ||||
| import $ from 'jquery'; | ||||
| import {attachTribute} from '../tribute.ts'; | ||||
| import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts'; | ||||
| import {initEasyMDEPaste, initTextareaEvents} from './EditorUpload.ts'; | ||||
| import { | ||||
|   EventUploadStateChanged, | ||||
|   initEasyMDEPaste, | ||||
|   initTextareaEvents, | ||||
|   triggerUploadStateChanged, | ||||
| } from './EditorUpload.ts'; | ||||
| import {handleGlobalEnterQuickSubmit} from './QuickSubmit.ts'; | ||||
| import {renderPreviewPanelContent} from '../repo-editor.ts'; | ||||
| import {easyMDEToolbarActions} from './EasyMDEToolbarActions.ts'; | ||||
| import {initTextExpander} from './TextExpander.ts'; | ||||
| import {showErrorToast} from '../../modules/toast.ts'; | ||||
| import {POST} from '../../modules/fetch.ts'; | ||||
| import {initTextareaMarkdown} from './EditorMarkdown.ts'; | ||||
| import {EventEditorContentChanged, initTextareaMarkdown, triggerEditorContentChanged} from './EditorMarkdown.ts'; | ||||
| import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts'; | ||||
|  | ||||
| let elementIdCounter = 0; | ||||
| @@ -37,7 +42,34 @@ export function validateTextareaNonEmpty(textarea) { | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| class ComboMarkdownEditor { | ||||
| export class ComboMarkdownEditor { | ||||
|   static EventEditorContentChanged = EventEditorContentChanged; | ||||
|   static EventUploadStateChanged = EventUploadStateChanged; | ||||
|  | ||||
|   public container : HTMLElement; | ||||
|  | ||||
|   // TODO: use correct types to replace these "any" types | ||||
|   options: any; | ||||
|  | ||||
|   tabEditor: HTMLElement; | ||||
|   tabPreviewer: HTMLElement; | ||||
|  | ||||
|   easyMDE: any; | ||||
|   easyMDEToolbarActions: any; | ||||
|   easyMDEToolbarDefault: any; | ||||
|  | ||||
|   textarea: HTMLTextAreaElement & {_giteaComboMarkdownEditor: any}; | ||||
|   textareaMarkdownToolbar: HTMLElement; | ||||
|   textareaAutosize: any; | ||||
|  | ||||
|   dropzone: HTMLElement; | ||||
|   attachedDropzoneInst: any; | ||||
|  | ||||
|   previewUrl: string; | ||||
|   previewContext: string; | ||||
|   previewMode: string; | ||||
|   previewWiki: boolean; | ||||
|  | ||||
|   constructor(container, options = {}) { | ||||
|     container._giteaComboMarkdownEditor = this; | ||||
|     this.options = options; | ||||
| @@ -63,14 +95,13 @@ class ComboMarkdownEditor { | ||||
|  | ||||
|   setupContainer() { | ||||
|     initTextExpander(this.container.querySelector('text-expander')); | ||||
|     this.container.addEventListener('ce-editor-content-changed', (e) => this.options?.onContentChanged?.(this, e)); | ||||
|   } | ||||
|  | ||||
|   setupTextarea() { | ||||
|     this.textarea = this.container.querySelector('.markdown-text-editor'); | ||||
|     this.textarea._giteaComboMarkdownEditor = this; | ||||
|     this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`; | ||||
|     this.textarea.addEventListener('input', (e) => this.options?.onContentChanged?.(this, e)); | ||||
|     this.textarea.addEventListener('input', () => triggerEditorContentChanged(this.container)); | ||||
|     this.applyEditorHeights(this.textarea, this.options.editorHeights); | ||||
|  | ||||
|     if (this.textarea.getAttribute('data-disable-autosize') !== 'true') { | ||||
| @@ -115,15 +146,21 @@ class ComboMarkdownEditor { | ||||
|  | ||||
|   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); | ||||
|     } | ||||
|     if (!dropzoneParentContainer) return; | ||||
|     this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone'); | ||||
|     if (!this.dropzone) return; | ||||
|  | ||||
|     this.attachedDropzoneInst = await initDropzone(this.dropzone); | ||||
|     // dropzone events | ||||
|     // * "processing" means a file is being uploaded | ||||
|     // * "queuecomplete" means all files have been uploaded | ||||
|     this.attachedDropzoneInst.on('processing', () => triggerUploadStateChanged(this.container)); | ||||
|     this.attachedDropzoneInst.on('queuecomplete', () => triggerUploadStateChanged(this.container)); | ||||
|   } | ||||
|  | ||||
|   dropzoneGetFiles() { | ||||
|     if (!this.dropzone) return null; | ||||
|     return Array.from(this.dropzone.querySelectorAll('.files [name=files]'), (el) => el.value); | ||||
|     return Array.from(this.dropzone.querySelectorAll<HTMLInputElement>('.files [name=files]'), (el) => el.value); | ||||
|   } | ||||
|  | ||||
|   dropzoneReloadFiles() { | ||||
| @@ -137,8 +174,13 @@ class ComboMarkdownEditor { | ||||
|     this.attachedDropzoneInst.emit(DropzoneCustomEventReloadFiles); | ||||
|   } | ||||
|  | ||||
|   isUploading() { | ||||
|     if (!this.dropzone) return false; | ||||
|     return this.attachedDropzoneInst.getQueuedFiles().length || this.attachedDropzoneInst.getUploadingFiles().length; | ||||
|   } | ||||
|  | ||||
|   setupTab() { | ||||
|     const tabs = this.container.querySelectorAll('.tabular.menu > .item'); | ||||
|     const tabs = this.container.querySelectorAll<HTMLElement>('.tabular.menu > .item'); | ||||
|  | ||||
|     // Fomantic Tab requires the "data-tab" to be globally unique. | ||||
|     // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic. | ||||
| @@ -170,7 +212,7 @@ class ComboMarkdownEditor { | ||||
|       formData.append('mode', this.previewMode); | ||||
|       formData.append('context', this.previewContext); | ||||
|       formData.append('text', this.value()); | ||||
|       formData.append('wiki', this.previewWiki); | ||||
|       formData.append('wiki', String(this.previewWiki)); | ||||
|       const response = await POST(this.previewUrl, {data: formData}); | ||||
|       const data = await response.text(); | ||||
|       renderPreviewPanelContent($(panelPreviewer), data); | ||||
| @@ -237,24 +279,24 @@ class ComboMarkdownEditor { | ||||
|     easyMDEOpt.toolbar = this.parseEasyMDEToolbar(EasyMDE, easyMDEOpt.toolbar ?? this.easyMDEToolbarDefault); | ||||
|  | ||||
|     this.easyMDE = new EasyMDE(easyMDEOpt); | ||||
|     this.easyMDE.codemirror.on('change', (...args) => {this.options?.onContentChanged?.(this, ...args)}); | ||||
|     this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container)); | ||||
|     this.easyMDE.codemirror.setOption('extraKeys', { | ||||
|       'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), | ||||
|       'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), | ||||
|       Enter: (cm) => { | ||||
|         const tributeContainer = document.querySelector('.tribute-container'); | ||||
|         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); | ||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|           cm.execCommand('newlineAndIndent'); | ||||
|         } | ||||
|       }, | ||||
|       Up: (cm) => { | ||||
|         const tributeContainer = document.querySelector('.tribute-container'); | ||||
|         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); | ||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|           return cm.execCommand('goLineUp'); | ||||
|         } | ||||
|       }, | ||||
|       Down: (cm) => { | ||||
|         const tributeContainer = document.querySelector('.tribute-container'); | ||||
|         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); | ||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|           return cm.execCommand('goLineDown'); | ||||
|         } | ||||
| @@ -314,13 +356,7 @@ export function getComboMarkdownEditor(el) { | ||||
|   return el?._giteaComboMarkdownEditor; | ||||
| } | ||||
|  | ||||
| export async function initComboMarkdownEditor(container, options = {}) { | ||||
|   if (container instanceof $) { | ||||
|     if (container.length !== 1) { | ||||
|       throw new Error('initComboMarkdownEditor: container must be a single element'); | ||||
|     } | ||||
|     container = container[0]; | ||||
|   } | ||||
| export async function initComboMarkdownEditor(container: HTMLElement, options = {}) { | ||||
|   if (!container) { | ||||
|     throw new Error('initComboMarkdownEditor: container is null'); | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| export const EventEditorContentChanged = 'ce-editor-content-changed'; | ||||
|  | ||||
| export function triggerEditorContentChanged(target) { | ||||
|   target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true})); | ||||
|   target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true})); | ||||
| } | ||||
|  | ||||
| function handleIndentSelection(textarea, e) { | ||||
|   | ||||
| @@ -7,9 +7,16 @@ import { | ||||
|   DropzoneCustomEventUploadDone, | ||||
|   generateMarkdownLinkForAttachment, | ||||
| } from '../dropzone.ts'; | ||||
| import type CodeMirror from 'codemirror'; | ||||
|  | ||||
| let uploadIdCounter = 0; | ||||
|  | ||||
| export const EventUploadStateChanged = 'ce-upload-state-changed'; | ||||
|  | ||||
| export function triggerUploadStateChanged(target) { | ||||
|   target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true})); | ||||
| } | ||||
|  | ||||
| function uploadFile(dropzoneEl, file) { | ||||
|   return new Promise((resolve) => { | ||||
|     const curUploadId = uploadIdCounter++; | ||||
| @@ -18,7 +25,7 @@ function uploadFile(dropzoneEl, file) { | ||||
|     const onUploadDone = ({file}) => { | ||||
|       if (file._giteaUploadId === curUploadId) { | ||||
|         dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone); | ||||
|         resolve(); | ||||
|         resolve(file); | ||||
|       } | ||||
|     }; | ||||
|     dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone); | ||||
| @@ -27,6 +34,8 @@ function uploadFile(dropzoneEl, file) { | ||||
| } | ||||
|  | ||||
| class TextareaEditor { | ||||
|   editor : HTMLTextAreaElement; | ||||
|  | ||||
|   constructor(editor) { | ||||
|     this.editor = editor; | ||||
|   } | ||||
| @@ -61,6 +70,8 @@ class TextareaEditor { | ||||
| } | ||||
|  | ||||
| class CodeMirrorEditor { | ||||
|   editor: CodeMirror.EditorFromTextArea; | ||||
|  | ||||
|   constructor(editor) { | ||||
|     this.editor = editor; | ||||
|   } | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| import $ from 'jquery'; | ||||
| import {handleReply} from './repo-issue.ts'; | ||||
| import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; | ||||
| import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; | ||||
| import {POST} from '../modules/fetch.ts'; | ||||
| import {showErrorToast} from '../modules/toast.ts'; | ||||
| import {hideElem, showElem} from '../utils/dom.ts'; | ||||
| import {attachRefIssueContextPopup} from './contextpopup.ts'; | ||||
| import {initCommentContent, initMarkupContent} from '../markup/content.ts'; | ||||
| import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; | ||||
|  | ||||
| async function onEditContent(event) { | ||||
|   event.preventDefault(); | ||||
| @@ -15,7 +16,7 @@ async function onEditContent(event) { | ||||
|   const renderContent = segment.querySelector('.render-content'); | ||||
|   const rawContent = segment.querySelector('.raw-content'); | ||||
|  | ||||
|   let comboMarkdownEditor; | ||||
|   let comboMarkdownEditor : ComboMarkdownEditor; | ||||
|  | ||||
|   const cancelAndReset = (e) => { | ||||
|     e.preventDefault(); | ||||
| @@ -79,9 +80,12 @@ async function onEditContent(event) { | ||||
|   comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); | ||||
|   if (!comboMarkdownEditor) { | ||||
|     editContentZone.innerHTML = document.querySelector('#issue-comment-editor-template').innerHTML; | ||||
|     const saveButton = editContentZone.querySelector('.ui.primary.button'); | ||||
|     comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); | ||||
|     const syncUiState = () => saveButton.disabled = comboMarkdownEditor.isUploading(); | ||||
|     comboMarkdownEditor.container.addEventListener(ComboMarkdownEditor.EventUploadStateChanged, syncUiState); | ||||
|     editContentZone.querySelector('.ui.cancel.button').addEventListener('click', cancelAndReset); | ||||
|     editContentZone.querySelector('.ui.primary.button').addEventListener('click', saveAndRefresh); | ||||
|     saveButton.addEventListener('click', saveAndRefresh); | ||||
|   } | ||||
|  | ||||
|   // Show write/preview tab and copy raw content as needed | ||||
| @@ -93,6 +97,7 @@ async function onEditContent(event) { | ||||
|   } | ||||
|   comboMarkdownEditor.switchTabToEditor(); | ||||
|   comboMarkdownEditor.focus(); | ||||
|   triggerUploadStateChanged(comboMarkdownEditor.container); | ||||
| } | ||||
|  | ||||
| export function initRepoIssueCommentEdit() { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import {htmlEscape} from 'escape-goat'; | ||||
| import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts'; | ||||
| import {hideElem, showElem, toggleElem} from '../utils/dom.ts'; | ||||
| import {setFileFolding} from './file-fold.ts'; | ||||
| import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; | ||||
| import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; | ||||
| import {toAbsoluteUrl} from '../utils.ts'; | ||||
| import {GET, POST} from '../modules/fetch.ts'; | ||||
| import {showErrorToast} from '../modules/toast.ts'; | ||||
| @@ -483,9 +483,9 @@ export function initRepoPullRequestReview() { | ||||
|     await handleReply(this); | ||||
|   }); | ||||
|  | ||||
|   const $reviewBox = $('.review-box-panel'); | ||||
|   if ($reviewBox.length === 1) { | ||||
|     const _promise = initComboMarkdownEditor($reviewBox.find('.combo-markdown-editor')); | ||||
|   const elReviewBox = document.querySelector('.review-box-panel'); | ||||
|   if (elReviewBox) { | ||||
|     initComboMarkdownEditor(elReviewBox.querySelector('.combo-markdown-editor')); | ||||
|   } | ||||
|  | ||||
|   // The following part is only for diff views | ||||
| @@ -548,7 +548,7 @@ 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); | ||||
|         const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor')); | ||||
|         const editor = await initComboMarkdownEditor($td[0].querySelector('.combo-markdown-editor')); | ||||
|         editor.focus(); | ||||
|       } catch (error) { | ||||
|         console.error(error); | ||||
| @@ -669,20 +669,22 @@ export async function initSingleCommentEditor($commentForm) { | ||||
|   // pages: | ||||
|   // * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content) | ||||
|   // * issue/pr view page: with comment form, has status-button and comment-button | ||||
|   const opts = {}; | ||||
|   const statusButton = document.querySelector('#status-button'); | ||||
|   const commentButton = document.querySelector('#comment-button'); | ||||
|   opts.onContentChanged = (editor) => { | ||||
|     const editorText = editor.value().trim(); | ||||
|   const editor = await initComboMarkdownEditor($commentForm[0].querySelector('.combo-markdown-editor')); | ||||
|   const statusButton = document.querySelector<HTMLButtonElement>('#status-button'); | ||||
|   const commentButton = document.querySelector<HTMLButtonElement>('#comment-button'); | ||||
|   const syncUiState = () => { | ||||
|     const editorText = editor.value().trim(), isUploading = editor.isUploading(); | ||||
|     if (statusButton) { | ||||
|       statusButton.textContent = statusButton.getAttribute(editorText ? 'data-status-and-comment' : 'data-status'); | ||||
|       statusButton.disabled = isUploading; | ||||
|     } | ||||
|     if (commentButton) { | ||||
|       commentButton.disabled = !editorText; | ||||
|       commentButton.disabled = !editorText || isUploading; | ||||
|     } | ||||
|   }; | ||||
|   const editor = await initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts); | ||||
|   opts.onContentChanged(editor); // sync state of buttons with the initial content | ||||
|   editor.container.addEventListener(ComboMarkdownEditor.EventUploadStateChanged, syncUiState); | ||||
|   editor.container.addEventListener(ComboMarkdownEditor.EventEditorContentChanged, syncUiState); | ||||
|   syncUiState(); | ||||
| } | ||||
|  | ||||
| export function initIssueTemplateCommentEditors($commentForm) { | ||||
| @@ -690,16 +692,13 @@ export function initIssueTemplateCommentEditors($commentForm) { | ||||
|   // * new issue with issue template | ||||
|   const $comboFields = $commentForm.find('.combo-editor-dropzone'); | ||||
|  | ||||
|   const initCombo = async ($combo) => { | ||||
|     const $dropzoneContainer = $combo.find('.form-field-dropzone'); | ||||
|     const $formField = $combo.find('.form-field-real'); | ||||
|     const $markdownEditor = $combo.find('.combo-markdown-editor'); | ||||
|   const initCombo = async (elCombo) => { | ||||
|     const $formField = $(elCombo.querySelector('.form-field-real')); | ||||
|     const dropzoneContainer = elCombo.querySelector('.form-field-dropzone'); | ||||
|     const markdownEditor = elCombo.querySelector('.combo-markdown-editor'); | ||||
|  | ||||
|     const editor = await initComboMarkdownEditor($markdownEditor, { | ||||
|       onContentChanged: (editor) => { | ||||
|         $formField.val(editor.value()); | ||||
|       }, | ||||
|     }); | ||||
|     const editor = await initComboMarkdownEditor(markdownEditor); | ||||
|     editor.container.addEventListener(ComboMarkdownEditor.EventEditorContentChanged, () => $formField.val(editor.value())); | ||||
|  | ||||
|     $formField.on('focus', async () => { | ||||
|       // deactivate all markdown editors | ||||
| @@ -709,8 +708,8 @@ export function initIssueTemplateCommentEditors($commentForm) { | ||||
|  | ||||
|       // activate this markdown editor | ||||
|       hideElem($formField); | ||||
|       showElem($markdownEditor); | ||||
|       showElem($dropzoneContainer); | ||||
|       showElem(markdownEditor); | ||||
|       showElem(dropzoneContainer); | ||||
|  | ||||
|       await editor.switchToUserPreference(); | ||||
|       editor.focus(); | ||||
| @@ -718,7 +717,7 @@ export function initIssueTemplateCommentEditors($commentForm) { | ||||
|   }; | ||||
|  | ||||
|   for (const el of $comboFields) { | ||||
|     initCombo($(el)); | ||||
|     initCombo(el); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -50,7 +50,7 @@ function initTagNameEditor() { | ||||
| } | ||||
|  | ||||
| function initRepoReleaseEditor() { | ||||
|   const editor = document.querySelector('.repository.new.release .combo-markdown-editor'); | ||||
|   const editor = document.querySelector<HTMLElement>('.repository.new.release .combo-markdown-editor'); | ||||
|   if (!editor) { | ||||
|     return; | ||||
|   } | ||||
|   | ||||
| @@ -4,11 +4,11 @@ import {fomanticMobileScreen} from '../modules/fomantic.ts'; | ||||
| import {POST} from '../modules/fetch.ts'; | ||||
|  | ||||
| async function initRepoWikiFormEditor() { | ||||
|   const editArea = document.querySelector('.repository.wiki .combo-markdown-editor textarea'); | ||||
|   const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea'); | ||||
|   if (!editArea) return; | ||||
|  | ||||
|   const form = document.querySelector('.repository.wiki.new .ui.form'); | ||||
|   const editorContainer = form.querySelector('.combo-markdown-editor'); | ||||
|   const editorContainer = form.querySelector<HTMLElement>('.combo-markdown-editor'); | ||||
|   let editor; | ||||
|  | ||||
|   let renderRequesting = false; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user