mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
refactor: replace Fomantic modal with native dialog (#39846)
Replace Fomantic's hard-forked jQuery modal with native `<dialog>` elements, advancing https://github.com/go-gitea/gitea/issues/29849. The UI is visually unchanged. Dialogs open with `show()` over an `inert` page, so tooltips, popups and toasts mounted on `<body>` stay usable above them, unlike with `showModal()` (https://github.com/whatwg/html/issues/9936). - Remove the Fomantic modal and dimmer, add `showModal` and `hideModal` - Declare modals as `<dialog class="modal-dialog">`, shown inside a `.modal-backdrop`, instead of `ui modal` with its unused size classes - Remove two modals that could never be opened - Block the page while a modal is open, close the topmost one on Escape and restore focus to its opener - Allow stacked modals, use a confirm modal instead of `window.confirm` for deleting content history - Hide popups and toasts when a modal opens or closes - Keep the release notes modal open when generating notes fails Fixes: https://github.com/go-gitea/gitea/issues/39643 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
1 parent
7610336d74
commit
e645afbb3f
95 files changed
+607
-1719
No files matched your search
@@ -1,31 +0,0 @@
|
||||
import {queryElemChildren} from '../../utils/dom.ts';
|
||||
|
||||
export function initFomanticDimmer() {
|
||||
// stand-in for removed dimmer module
|
||||
$.fn.dimmer = function (this: any, arg0: string, arg1: any) {
|
||||
if (arg0 === 'add content') {
|
||||
const $el = arg1;
|
||||
const existingDimmer = document.querySelector('body > .ui.dimmer');
|
||||
if (existingDimmer) {
|
||||
queryElemChildren(existingDimmer, '*', (el) => el.classList.add('hidden'));
|
||||
this._dimmer = existingDimmer;
|
||||
} else {
|
||||
this._dimmer = document.createElement('div');
|
||||
this._dimmer.classList.add('ui', 'dimmer');
|
||||
document.body.append(this._dimmer);
|
||||
}
|
||||
this._dimmer.append($el[0]);
|
||||
} else if (arg0 === 'get dimmer') {
|
||||
return $(this._dimmer);
|
||||
} else if (arg0 === 'show') {
|
||||
this._dimmer.classList.add('active');
|
||||
document.body.classList.add('tw-overflow-hidden');
|
||||
} else if (arg0 === 'hide') {
|
||||
const cb = arg1;
|
||||
this._dimmer.classList.remove('active');
|
||||
document.body.classList.remove('tw-overflow-hidden');
|
||||
cb();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import type {FomanticInitFunction, JQueryElem} from '../../types.ts';
|
||||
import {queryElems} from '../../utils/dom.ts';
|
||||
import {hideToastsFrom} from '../toast.ts';
|
||||
|
||||
type ModalOpts = {
|
||||
closable?: boolean;
|
||||
onApprove?: (this: HTMLElement) => boolean | void;
|
||||
onShow?: (this: HTMLElement) => void | Promise<void>;
|
||||
onHide?: (this: HTMLElement) => void;
|
||||
onHidden?: (this: HTMLElement) => void;
|
||||
};
|
||||
|
||||
// thin wrapper around Fomantic's jQuery modal plugin so callers don't have to touch jQuery or fomanticQuery
|
||||
export function showFomanticModal(el: Element | null, opts: ModalOpts = {}) {
|
||||
if (!el) return;
|
||||
const $el = $(el);
|
||||
if (Object.keys(opts).length) $el.modal(opts);
|
||||
$el.modal('show');
|
||||
}
|
||||
|
||||
export function hideFomanticModal(el: Element | null) {
|
||||
if (!el) return;
|
||||
$(el).modal('hide');
|
||||
}
|
||||
|
||||
const fomanticModalFn = $.fn.modal;
|
||||
|
||||
// use our own `$.fn.modal` to patch Fomantic's modal module
|
||||
export function initAriaModalPatch() {
|
||||
if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once');
|
||||
$.fn.modal = ariaModalFn;
|
||||
(ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings;
|
||||
$.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden;
|
||||
$.fn.modal.settings.onApprove = onModalApproveDefault;
|
||||
}
|
||||
|
||||
// the patched `$.fn.modal` modal function
|
||||
// * it does the one-time attaching on the first call
|
||||
function ariaModalFn(this: JQueryElem, ...args: Parameters<FomanticInitFunction>) {
|
||||
const ret = fomanticModalFn.apply(this, args);
|
||||
if (args[0] === 'show' || args[0]?.autoShow) {
|
||||
for (const el of this) {
|
||||
el.querySelector('.close-modal')?.setAttribute('aria-label', 'Close');
|
||||
// If there is a form in the modal, there might be a "cancel" button before "ok" button (all buttons are "type=submit" by default).
|
||||
// In such case, the "Enter" key will trigger the "cancel" button instead of "ok" button, then the dialog will be closed.
|
||||
// It breaks the user experience - the "Enter" key should confirm the dialog and submit the form.
|
||||
// So, all "cancel" buttons without "[type]" must be marked as "type=button".
|
||||
// Although there are lint rules for "button type", some HTML code is generated in JS/Vue/Go which isn't linted, so we still need this patch.
|
||||
for (const button of el.querySelectorAll('form button.cancel:not([type])')) {
|
||||
button.setAttribute('type', 'button');
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function onModalBeforeHidden(this: HTMLElement) {
|
||||
const $modal = $(this);
|
||||
const elModal = $modal[0];
|
||||
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: HTMLElement) {
|
||||
const $modal = $(this);
|
||||
const selectors = $modal.modal('setting', 'selector');
|
||||
const elModal = $modal[0];
|
||||
const elApprove = elModal.querySelector<HTMLElement>(selectors.approve);
|
||||
const elForm = elApprove?.closest<HTMLFormElement>('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.
|
||||
if (!elForm.reportValidity()) return false;
|
||||
elForm.classList.add('is-loading');
|
||||
return false;
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
export function initFomanticTransition() {
|
||||
const transitionNopBehaviors = new Set([
|
||||
'clear queue', 'stop', 'stop all', 'destroy',
|
||||
'force repaint', 'repaint', 'reset',
|
||||
'looping', 'remove looping', 'disable', 'enable',
|
||||
'set duration', 'save conditions', 'restore conditions',
|
||||
]);
|
||||
// stand-in for removed transition module
|
||||
$.fn.transition = function (arg0: any, arg1?: number, arg2?: (this: HTMLElement) => void) {
|
||||
$.fn.transition = function (arg0: any) {
|
||||
if (arg0 === 'is supported') return true;
|
||||
if (arg0 === 'is animating') return false;
|
||||
if (arg0 === 'is inward') return false;
|
||||
@@ -14,10 +8,8 @@ export function initFomanticTransition() {
|
||||
|
||||
let argObj: Record<string, any>;
|
||||
if (typeof arg0 === 'string') {
|
||||
// many behaviors are no-op now. https://fomantic-ui.com/modules/transition.html#/usage
|
||||
if (transitionNopBehaviors.has(arg0)) return this;
|
||||
// now, the arg0 is an animation name, the syntax: (animation, duration, complete)
|
||||
argObj = {animation: arg0, ...(arg1 && {duration: arg1}), ...(arg2 && {onComplete: arg2})};
|
||||
if (arg0 === 'stop all') return this;
|
||||
argObj = {animation: arg0};
|
||||
} else if (typeof arg0 === 'object') {
|
||||
argObj = arg0;
|
||||
} else {
|
||||
@@ -38,12 +30,10 @@ export function initFomanticTransition() {
|
||||
el.classList.remove('hidden');
|
||||
el.classList.add('visible', 'transition');
|
||||
if (argObj.displayType) el.style.setProperty('display', argObj.displayType, 'important');
|
||||
argObj.onShow?.call(el);
|
||||
} else {
|
||||
el.classList.add('hidden');
|
||||
el.classList.remove('visible'); // don't remove the transition class because the Fomantic animation style is `.hidden.transition`.
|
||||
el.style.removeProperty('display');
|
||||
argObj.onHidden?.call(el);
|
||||
}
|
||||
argObj.onComplete?.call(el);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user