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,7 +1,7 @@
|
||||
import {checkAppUrl} from '../common-page.ts';
|
||||
import {hideElem, queryElems, showElem, toggleElem} from '../../utils/dom.ts';
|
||||
import {POST} from '../../modules/fetch.ts';
|
||||
import {showFomanticModal} from '../../modules/fomantic/modal.ts';
|
||||
import {showModal} from '../../modules/modal.ts';
|
||||
import {pathEscape} from '../../utils/url.ts';
|
||||
import {registerGlobalInitFunc} from '../../modules/observer.ts';
|
||||
|
||||
@@ -274,7 +274,7 @@ function initAdminNotice() {
|
||||
const pageContent = document.querySelector('.page-content.admin.notice');
|
||||
if (!pageContent) return;
|
||||
|
||||
const detailModal = document.querySelector<HTMLDivElement>('#detail-modal')!;
|
||||
const detailModal = document.querySelector<HTMLDialogElement>('#detail-modal')!;
|
||||
|
||||
// Attach view detail modals
|
||||
queryElems(pageContent, '.view-detail', (el) => el.addEventListener('click', (e) => {
|
||||
@@ -282,7 +282,7 @@ function initAdminNotice() {
|
||||
const elNoticeDesc = el.closest('tr')!.querySelector('.notice-description')!;
|
||||
const elModalDesc = detailModal.querySelector('.content pre')!;
|
||||
elModalDesc.textContent = elNoticeDesc.textContent;
|
||||
showFomanticModal(detailModal);
|
||||
showModal(detailModal);
|
||||
}));
|
||||
|
||||
// Select actions
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {showModal} from '../modules/modal.ts';
|
||||
import {camelize} from 'vue';
|
||||
import {applyAutoFocus} from './common-page.ts';
|
||||
|
||||
@@ -90,7 +90,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
|
||||
// to make the template variables get correctly escaped in the URL.
|
||||
e.preventDefault();
|
||||
const modalSelector = el.getAttribute('data-modal')!;
|
||||
const elModal = document.querySelector(modalSelector);
|
||||
const elModal = document.querySelector<HTMLDialogElement>(modalSelector);
|
||||
if (!elModal) throw new Error('no modal for this action');
|
||||
|
||||
const modalAttrPrefix = 'data-modal-';
|
||||
@@ -123,11 +123,11 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
showFomanticModal(elModal);
|
||||
showModal(elModal);
|
||||
}
|
||||
|
||||
export function initGlobalButtons(): void {
|
||||
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
|
||||
// There are many "cancel button" elements in modal dialogs, they are expected to be button-like elements but never submit a form.
|
||||
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
|
||||
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
|
||||
addDelegatedEventListener(document, 'click', 'form button.ui.cancel.button', (_ /* el */, e) => e.preventDefault());
|
||||
|
||||
@@ -126,7 +126,7 @@ function autoFocusEnd(el: HTMLInputElement | HTMLTextAreaElement) {
|
||||
export function applyAutoFocus(container: Element) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus
|
||||
// "autofocus" behavior is defined by the standard: when a container (e.g.: dialog) becomes visible, focus the element with "autofocus" attribute
|
||||
// Fomantic UI already supports it for its modal dialog, we need to cover more cases (e.g.: ".show-panel" button)
|
||||
// The native modal dialog already supports it, we need to cover more cases (e.g.: ".show-panel" button)
|
||||
// Here is just a simple support, we don't expect more than one element that need "autofocus" appearing in the same container
|
||||
container.querySelector<HTMLElement>('[autofocus]')?.focus();
|
||||
// Also, apply our autoFocusEnd behavior
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {svg} from '../../svg.ts';
|
||||
import {html, htmlRaw} from '../../utils/html.ts';
|
||||
import {createElementFromHTML} from '../../utils/dom.ts';
|
||||
import {showFomanticModal} from '../../modules/fomantic/modal.ts';
|
||||
import {hideToastsAll} from '../../modules/toast.ts';
|
||||
import {showModal} from '../../modules/modal.ts';
|
||||
|
||||
const {i18n} = window.config;
|
||||
|
||||
@@ -12,27 +11,24 @@ type ConfirmModalOptions = {
|
||||
confirmButtonColor?: 'primary' | 'red' | 'green' | 'blue';
|
||||
};
|
||||
|
||||
export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLElement {
|
||||
export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLDialogElement {
|
||||
const headerHtml = header ? html`<div class="header">${header}</div>` : '';
|
||||
return createElementFromHTML(html`
|
||||
<div class="ui g-modal-confirm modal">
|
||||
<dialog class="modal-dialog">
|
||||
${htmlRaw(headerHtml)}
|
||||
<div class="content">${content}</div>
|
||||
<div class="actions">
|
||||
<button class="ui cancel button">${htmlRaw(svg('octicon-x'))} ${i18n.modal_cancel}</button>
|
||||
<button class="ui ${confirmButtonColor} ok button" autofocus>${htmlRaw(svg('octicon-check'))} ${i18n.modal_confirm}</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
`.trim());
|
||||
}
|
||||
|
||||
export function confirmModal(modal: HTMLElement | ConfirmModalOptions): Promise<boolean> {
|
||||
if (!(modal instanceof HTMLElement)) modal = createConfirmModal(modal);
|
||||
// hide existing toasts when we need to show a new modal, otherwise the toasts only interfere the UI
|
||||
// it's fine to do so because the modal is triggered by user's explicit action, so the user should already have read the toast messages
|
||||
hideToastsAll();
|
||||
export function confirmModal(modal: HTMLDialogElement | ConfirmModalOptions): Promise<boolean> {
|
||||
if (!(modal instanceof HTMLDialogElement)) modal = createConfirmModal(modal);
|
||||
return new Promise((resolve) => {
|
||||
showFomanticModal(modal, {
|
||||
showModal(modal, {
|
||||
onApprove() {
|
||||
resolve(true);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {queryElems, toggleElem} from '../../utils/dom.ts';
|
||||
import {showFomanticModal} from '../../modules/fomantic/modal.ts';
|
||||
import {showModal} from '../../modules/modal.ts';
|
||||
import {submitFormFetchAction} from '../../modules/fetch-action.ts';
|
||||
import {registerGlobalInitFunc} from '../../modules/observer.ts';
|
||||
|
||||
@@ -7,7 +7,7 @@ function nameHasScope(name: string): boolean {
|
||||
return /.*[^/]\/[^/].*/.test(name);
|
||||
}
|
||||
|
||||
function initLabelEditModal(elModal: HTMLElement) {
|
||||
function initLabelEditModal(elModal: HTMLDialogElement) {
|
||||
const elLabelId = elModal.querySelector<HTMLInputElement>('input[name="id"]')!;
|
||||
const elNameInput = elModal.querySelector<HTMLInputElement>('.label-name-input')!;
|
||||
const elExclusiveField = elModal.querySelector('.label-exclusive-input-field')!;
|
||||
@@ -59,7 +59,7 @@ function initLabelEditModal(elModal: HTMLElement) {
|
||||
form.action = isEdit ? `${curPageLink}/edit` : `${curPageLink}/new`;
|
||||
toggleElem(elIsArchivedField, isEdit);
|
||||
syncModalUi();
|
||||
showFomanticModal(elModal, {
|
||||
showModal(elModal, {
|
||||
onApprove() {
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {toggleElem} from '../utils/dom.ts';
|
||||
import {showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {showModal} from '../modules/modal.ts';
|
||||
import {trString} from '../modules/i18n.ts';
|
||||
import {registerGlobalEventFunc} from '../modules/observer.ts';
|
||||
|
||||
@@ -20,7 +20,7 @@ function initRepoCreateBranchButton() {
|
||||
const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span';
|
||||
document.querySelector(fromSpanName)!.textContent = el.getAttribute('data-branch-from');
|
||||
|
||||
showFomanticModal(document.querySelector(el.getAttribute('data-modal')!));
|
||||
showModal(document.querySelector(el.getAttribute('data-modal')!)!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import {GET, POST} from '../modules/fetch.ts';
|
||||
import {createElementFromHTML, showElem} from '../utils/dom.ts';
|
||||
import {parseIssuePageInfo} from '../utils.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {hideFomanticModal, showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {hideModal, showModal} from '../modules/modal.ts';
|
||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import type {JQueryElem} from '../types.ts';
|
||||
|
||||
@@ -14,8 +15,8 @@ let i18nTextDeleteFromHistory: string;
|
||||
let i18nTextDeleteFromHistoryConfirm: string;
|
||||
|
||||
function showContentHistoryDetail(issueBaseUrl: string, commentId: string, historyId: string, itemTitleHtml: string) {
|
||||
const elDetailDialog = createElementFromHTML(html`
|
||||
<div class="ui modal content-history-detail-dialog">
|
||||
const elDetailDialog = createElementFromHTML<HTMLDialogElement>(html`
|
||||
<dialog class="modal-dialog content-history-detail-dialog">
|
||||
<div class="header flex-left-right">
|
||||
<div>${htmlRaw(itemTitleHtml)}</div>
|
||||
<div class="ui dropdown dialog-header-options tw-mx-4 tw-hidden">
|
||||
@@ -28,9 +29,8 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
|
||||
<button type="button" class="close-modal"></button>
|
||||
</div>
|
||||
<div class="comment-diff-data is-loading"></div>
|
||||
</div>
|
||||
</dialog>
|
||||
`);
|
||||
document.body.append(elDetailDialog);
|
||||
const elOptionsDropdown = elDetailDialog.querySelector('.ui.dropdown.dialog-header-options')!;
|
||||
const $fomanticDropdownOptions = fomanticQuery(elOptionsDropdown);
|
||||
$fomanticDropdownOptions.dropdown({
|
||||
@@ -39,8 +39,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
|
||||
async onChange(_value: string, _text: string, $item: JQueryElem) {
|
||||
const optionItem = $item.data('option-item');
|
||||
if (optionItem === 'delete') {
|
||||
// TODO: to use confirmModal instead of window.confirm, need to support multiple modals on the same page first.
|
||||
if (window.confirm(i18nTextDeleteFromHistoryConfirm)) {
|
||||
if (await confirmModal({content: i18nTextDeleteFromHistoryConfirm, confirmButtonColor: 'red'})) {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('comment_id', commentId);
|
||||
@@ -50,7 +49,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
|
||||
const resp = await response.json();
|
||||
|
||||
if (resp.ok) {
|
||||
hideFomanticModal(elDetailDialog);
|
||||
hideModal(elDetailDialog);
|
||||
} else {
|
||||
showErrorToast(resp.message);
|
||||
}
|
||||
@@ -67,7 +66,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
|
||||
$fomanticDropdownOptions.dropdown('clear', true);
|
||||
},
|
||||
});
|
||||
showFomanticModal(elDetailDialog, {
|
||||
showModal(elDetailDialog, {
|
||||
async onShow() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -14,7 +14,7 @@ import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} fr
|
||||
import {GET, POST} from '../modules/fetch.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {showModal} from '../modules/modal.ts';
|
||||
import {ignoreAreYouSure} from '../modules/are-you-sure.ts';
|
||||
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import type {FomanticApiResponse} from '../types.ts';
|
||||
@@ -325,10 +325,10 @@ export function initRepoIssueReferenceIssue() {
|
||||
const poster = el.getAttribute('data-poster-username');
|
||||
const reference = el.getAttribute('data-reference')!;
|
||||
const modalSelector = el.getAttribute('data-modal')!;
|
||||
const modal = document.querySelector(modalSelector)!;
|
||||
const modal = document.querySelector<HTMLDialogElement>(modalSelector)!;
|
||||
const textarea = modal.querySelector<HTMLTextAreaElement>('textarea[name="content"]')!;
|
||||
textarea.value = `${content}\n\n_Originally posted by @${poster} in ${reference}_`;
|
||||
showFomanticModal(modal);
|
||||
showModal(modal);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import {contrastColor} from '../utils/color.ts';
|
||||
import {createSortable} from '../modules/sortable.ts';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {performFetchActionRequest} from '../modules/fetch-action.ts';
|
||||
import {hideFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {hideModal} from '../modules/modal.ts';
|
||||
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
|
||||
import type {SortableEvent} from 'sortablejs';
|
||||
import {toggleFullScreen} from '../utils.ts';
|
||||
@@ -82,7 +82,7 @@ function initRepoProjectSortable(): void {
|
||||
}
|
||||
|
||||
function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
|
||||
const elModal = document.querySelector<HTMLElement>('.ui.modal#project-column-modal-edit')!;
|
||||
const elModal = document.querySelector<HTMLDialogElement>('#project-column-modal-edit')!;
|
||||
const elForm = elModal.querySelector<HTMLFormElement>('form')!;
|
||||
|
||||
const elColumnId = elForm.querySelector<HTMLInputElement>('input[name="id"]')!;
|
||||
@@ -140,7 +140,7 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
|
||||
queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.removeProperty('color'));
|
||||
}
|
||||
|
||||
hideFomanticModal(elModal);
|
||||
hideModal(elModal);
|
||||
} finally {
|
||||
elForm.classList.remove('is-loading');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
|
||||
import {getComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||
import {hideElem} from '../utils/dom.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {hideFomanticModal, showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {hideModal, showModal} from '../modules/modal.ts';
|
||||
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import {htmlEscape} from '../utils/html.ts';
|
||||
import {compareVersions} from 'compare-versions';
|
||||
@@ -84,7 +84,7 @@ function initGenerateReleaseNotes(elForm: HTMLFormElement) {
|
||||
const textMissingTag = buttonShowModal.getAttribute('data-text-missing-tag')!;
|
||||
const generateUrl = buttonShowModal.getAttribute('data-generate-url')!;
|
||||
|
||||
const elModal = document.querySelector('#generate-release-notes-modal')!;
|
||||
const elModal = document.querySelector<HTMLDialogElement>('#generate-release-notes-modal')!;
|
||||
|
||||
const doSubmit = async (tagName: string) => {
|
||||
const elPreviousTag = elModal.querySelector<HTMLSelectElement>('[name=previous_tag]')!;
|
||||
@@ -111,18 +111,18 @@ function initGenerateReleaseNotes(elForm: HTMLFormElement) {
|
||||
} else {
|
||||
comboEditor.value(data.content);
|
||||
}
|
||||
hideModal(elModal);
|
||||
comboEditor.focus();
|
||||
} finally {
|
||||
elModal.classList.remove('loading', 'disabled');
|
||||
hideFomanticModal(elModal);
|
||||
comboEditor.focus();
|
||||
}
|
||||
};
|
||||
|
||||
let inited = false;
|
||||
const doShowModal = () => {
|
||||
hideToastsAll();
|
||||
const tagName = tagNameInput.value.trim();
|
||||
if (!tagName) {
|
||||
hideToastsAll();
|
||||
showErrorToast(textMissingTag, {duration: 3000});
|
||||
tagNameInput.focus();
|
||||
return;
|
||||
@@ -140,7 +140,7 @@ function initGenerateReleaseNotes(elForm: HTMLFormElement) {
|
||||
}
|
||||
$dropdown.dropdown('set selected', guessPreviousReleaseTag(tagName, existingTags));
|
||||
|
||||
showFomanticModal(elModal, {
|
||||
showModal(elModal, {
|
||||
onApprove: () => {
|
||||
doSubmit(tagName); // don't await, need to return false to keep the modal
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user