mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 01:58:56 +00:00 
			
		
		
		
	Fix modal + form abuse (#34921)
See the comment. And due to the abuse, there is a regression: when the modal is hidden, the form will be reset and it can't submit. This PR fixes all problems: keep the modal with form open, and add "loading" indicator.
This commit is contained in:
		| @@ -43,13 +43,16 @@ export function initGlobalDeleteButton(): void { | |||||||
|  |  | ||||||
|       fomanticQuery(modal).modal({ |       fomanticQuery(modal).modal({ | ||||||
|         closable: false, |         closable: false, | ||||||
|         onApprove: async () => { |         onApprove: () => { | ||||||
|           // if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."` |           // if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."` | ||||||
|           if (btn.getAttribute('data-type') === 'form') { |           if (btn.getAttribute('data-type') === 'form') { | ||||||
|             const formSelector = btn.getAttribute('data-form'); |             const formSelector = btn.getAttribute('data-form'); | ||||||
|             const form = document.querySelector<HTMLFormElement>(formSelector); |             const form = document.querySelector<HTMLFormElement>(formSelector); | ||||||
|             if (!form) throw new Error(`no form named ${formSelector} found`); |             if (!form) throw new Error(`no form named ${formSelector} found`); | ||||||
|  |             modal.classList.add('is-loading'); // the form is not in the modal, so also add loading indicator to the modal | ||||||
|  |             form.classList.add('is-loading'); | ||||||
|             form.submit(); |             form.submit(); | ||||||
|  |             return false; // prevent modal from closing automatically | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           // prepare an AJAX form by data attributes |           // prepare an AJAX form by data attributes | ||||||
| @@ -62,12 +65,15 @@ export function initGlobalDeleteButton(): void { | |||||||
|               postData.append('id', value); |               postData.append('id', value); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|  |           (async () => { | ||||||
|           const response = await POST(btn.getAttribute('data-url'), {data: postData}); |             const response = await POST(btn.getAttribute('data-url'), {data: postData}); | ||||||
|           if (response.ok) { |             if (response.ok) { | ||||||
|             const data = await response.json(); |               const data = await response.json(); | ||||||
|             window.location.href = data.redirect; |               window.location.href = data.redirect; | ||||||
|           } |             } | ||||||
|  |           })(); | ||||||
|  |           modal.classList.add('is-loading'); // the request is in progress, so also add loading indicator to the modal | ||||||
|  |           return false; // prevent modal from closing automatically | ||||||
|         }, |         }, | ||||||
|       }).modal('show'); |       }).modal('show'); | ||||||
|     }); |     }); | ||||||
| @@ -158,13 +164,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   fomanticQuery(elModal).modal('setting', { |   fomanticQuery(elModal).modal('show'); | ||||||
|     onApprove: () => { |  | ||||||
|       // "form-fetch-action" can handle network errors gracefully, |  | ||||||
|       // so keep the modal dialog to make users can re-submit the form if anything wrong happens. |  | ||||||
|       if (elModal.querySelector('.form-fetch-action')) return false; |  | ||||||
|     }, |  | ||||||
|   }).modal('show'); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export function initGlobalButtons(): void { | export function initGlobalButtons(): void { | ||||||
|   | |||||||
| @@ -72,6 +72,7 @@ export function initCompLabelEdit(pageSelector: string) { | |||||||
|           return false; |           return false; | ||||||
|         } |         } | ||||||
|         submitFormFetchAction(form); |         submitFormFetchAction(form); | ||||||
|  |         return false; | ||||||
|       }, |       }, | ||||||
|     }).modal('show'); |     }).modal('show'); | ||||||
|   }; |   }; | ||||||
|   | |||||||
| @@ -9,8 +9,9 @@ const fomanticModalFn = $.fn.modal; | |||||||
| export function initAriaModalPatch() { | export function initAriaModalPatch() { | ||||||
|   if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once'); |   if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once'); | ||||||
|   $.fn.modal = ariaModalFn; |   $.fn.modal = ariaModalFn; | ||||||
|   $.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden; |  | ||||||
|   (ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings; |   (ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings; | ||||||
|  |   $.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden; | ||||||
|  |   $.fn.modal.settings.onApprove = onModalApproveDefault; | ||||||
| } | } | ||||||
|  |  | ||||||
| // the patched `$.fn.modal` modal function | // the patched `$.fn.modal` modal function | ||||||
| @@ -34,6 +35,29 @@ function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) { | |||||||
| function onModalBeforeHidden(this: any) { | function onModalBeforeHidden(this: any) { | ||||||
|   const $modal = $(this); |   const $modal = $(this); | ||||||
|   const elModal = $modal[0]; |   const elModal = $modal[0]; | ||||||
|   queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset()); |  | ||||||
|   hideToastsFrom(elModal.closest('.ui.dimmer') ?? document.body); |   hideToastsFrom(elModal.closest('.ui.dimmer') ?? document.body); | ||||||
|  |  | ||||||
|  |   // reset the form after the modal is hidden, after other modal events and handlers (e.g. "onApprove", form submit) | ||||||
|  |   setTimeout(() => { | ||||||
|  |     queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset()); | ||||||
|  |   }, 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function onModalApproveDefault(this: any) { | ||||||
|  |   const $modal = $(this); | ||||||
|  |   const selectors = $modal.modal('setting', 'selector'); | ||||||
|  |   const elModal = $modal[0]; | ||||||
|  |   const elApprove = elModal.querySelector(selectors.approve); | ||||||
|  |   const elForm = elApprove?.closest('form'); | ||||||
|  |   if (!elForm) return true; // no form, just allow closing the modal | ||||||
|  |  | ||||||
|  |   // "form-fetch-action" can handle network errors gracefully, | ||||||
|  |   // so keep the modal dialog to make users can re-submit the form if anything wrong happens. | ||||||
|  |   if (elForm.matches('.form-fetch-action')) return false; | ||||||
|  |  | ||||||
|  |   // There is an abuse for the "modal" + "form" combination, the "Approve" button is a traditional form submit button in the form. | ||||||
|  |   // Then "approve" and "submit" occur at the same time, the modal will be closed immediately before the form is submitted. | ||||||
|  |   // So here we prevent the modal from closing automatically by returning false, add the "is-loading" class to the form element. | ||||||
|  |   elForm.classList.add('is-loading'); | ||||||
|  |   return false; | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user