mirror of
https://github.com/go-gitea/gitea
synced 2024-12-27 10:25:57 +01:00
0f4b0cf892
1. correct the modal usage on "admin email list" page (then `web_src/js/features/admin/emails.ts` is removed) 2. use `addDelegatedEventListener` instead of `jQuery().on` 3. more jQuery related changes and remove jQuery from `web_src/js/features/common-button.ts` 4. improve `confirmModal` to make it support header, and remove incorrect double-escaping 5. fix more typescript related types 6. fine tune devtest pages and add more tests
166 lines
7.0 KiB
TypeScript
166 lines
7.0 KiB
TypeScript
import {POST} from '../modules/fetch.ts';
|
|
import {addDelegatedEventListener, hideElem, queryElems, showElem, toggleElem} from '../utils/dom.ts';
|
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
|
import {camelize} from 'vue';
|
|
|
|
export function initGlobalButtonClickOnEnter(): void {
|
|
addDelegatedEventListener(document, 'keypress', 'div.ui.button, span.ui.button', (el, e: KeyboardEvent) => {
|
|
if (e.code === 'Space' || e.code === 'Enter') {
|
|
e.preventDefault();
|
|
el.click();
|
|
}
|
|
});
|
|
}
|
|
|
|
export function initGlobalDeleteButton(): void {
|
|
// ".delete-button" shows a confirmation modal defined by `data-modal-id` attribute.
|
|
// Some model/form elements will be filled by `data-id` / `data-name` / `data-data-xxx` attributes.
|
|
// If there is a form defined by `data-form`, then the form will be submitted as-is (without any modification).
|
|
// If there is no form, then the data will be posted to `data-url`.
|
|
// TODO: it's not encouraged to use this method. `show-modal` does far better than this.
|
|
for (const btn of document.querySelectorAll<HTMLElement>('.delete-button')) {
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
|
|
// eslint-disable-next-line github/no-dataset -- code depends on the camel-casing
|
|
const dataObj = btn.dataset;
|
|
|
|
const modalId = btn.getAttribute('data-modal-id');
|
|
const modal = document.querySelector(`.delete.modal${modalId ? `#${modalId}` : ''}`);
|
|
|
|
// set the modal "display name" by `data-name`
|
|
const modalNameEl = modal.querySelector('.name');
|
|
if (modalNameEl) modalNameEl.textContent = btn.getAttribute('data-name');
|
|
|
|
// fill the modal elements with data-xxx attributes: `data-data-organization-name="..."` => `<span class="dataOrganizationName">...</span>`
|
|
for (const [key, value] of Object.entries(dataObj)) {
|
|
if (key.startsWith('data')) {
|
|
const textEl = modal.querySelector(`.${key}`);
|
|
if (textEl) textEl.textContent = value;
|
|
}
|
|
}
|
|
|
|
fomanticQuery(modal).modal({
|
|
closable: false,
|
|
onApprove: async () => {
|
|
// if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."`
|
|
if (btn.getAttribute('data-type') === 'form') {
|
|
const formSelector = btn.getAttribute('data-form');
|
|
const form = document.querySelector<HTMLFormElement>(formSelector);
|
|
if (!form) throw new Error(`no form named ${formSelector} found`);
|
|
form.submit();
|
|
}
|
|
|
|
// prepare an AJAX form by data attributes
|
|
const postData = new FormData();
|
|
for (const [key, value] of Object.entries(dataObj)) {
|
|
if (key.startsWith('data')) { // for data-data-xxx (HTML) -> dataXxx (form)
|
|
postData.append(key.slice(4), value);
|
|
}
|
|
if (key === 'id') { // for data-id="..."
|
|
postData.append('id', value);
|
|
}
|
|
}
|
|
|
|
const response = await POST(btn.getAttribute('data-url'), {data: postData});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
window.location.href = data.redirect;
|
|
}
|
|
},
|
|
}).modal('show');
|
|
});
|
|
}
|
|
}
|
|
|
|
function onShowPanelClick(e) {
|
|
// a '.show-panel' element can show a panel, by `data-panel="selector"`
|
|
// if it has "toggle" class, it toggles the panel
|
|
const el = e.currentTarget;
|
|
e.preventDefault();
|
|
const sel = el.getAttribute('data-panel');
|
|
if (el.classList.contains('toggle')) {
|
|
toggleElem(sel);
|
|
} else {
|
|
showElem(sel);
|
|
}
|
|
}
|
|
|
|
function onHidePanelClick(e) {
|
|
// a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
|
|
const el = e.currentTarget;
|
|
e.preventDefault();
|
|
let sel = el.getAttribute('data-panel');
|
|
if (sel) {
|
|
hideElem(sel);
|
|
return;
|
|
}
|
|
sel = el.getAttribute('data-panel-closest');
|
|
if (sel) {
|
|
hideElem(el.parentNode.closest(sel));
|
|
return;
|
|
}
|
|
throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code
|
|
}
|
|
|
|
function onShowModalClick(e) {
|
|
// A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
|
|
// Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
|
|
// * First, try to query '#target'
|
|
// * Then, try to query '[name=target]'
|
|
// * Then, try to query '.target'
|
|
// * Then, try to query 'target' as HTML tag
|
|
// If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
|
|
const el = e.currentTarget;
|
|
e.preventDefault();
|
|
const modalSelector = el.getAttribute('data-modal');
|
|
const elModal = document.querySelector(modalSelector);
|
|
if (!elModal) throw new Error('no modal for this action');
|
|
|
|
const modalAttrPrefix = 'data-modal-';
|
|
for (const attrib of el.attributes) {
|
|
if (!attrib.name.startsWith(modalAttrPrefix)) {
|
|
continue;
|
|
}
|
|
|
|
const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length);
|
|
const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.');
|
|
// try to find target by: "#target" -> "[name=target]" -> ".target" -> "<target> tag"
|
|
const attrTarget = elModal.querySelector(`#${attrTargetName}`) ||
|
|
elModal.querySelector(`[name=${attrTargetName}]`) ||
|
|
elModal.querySelector(`.${attrTargetName}`) ||
|
|
elModal.querySelector(`${attrTargetName}`);
|
|
if (!attrTarget) {
|
|
if (!window.config.runModeIsProd) throw new Error(`attr target "${attrTargetCombo}" not found for modal`);
|
|
continue;
|
|
}
|
|
|
|
if (attrTargetAttr) {
|
|
attrTarget[camelize(attrTargetAttr)] = attrib.value;
|
|
} else if (attrTarget.matches('input, textarea')) {
|
|
attrTarget.value = attrib.value; // FIXME: add more supports like checkbox
|
|
} else {
|
|
attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p
|
|
}
|
|
}
|
|
|
|
fomanticQuery(elModal).modal('setting', {
|
|
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 {
|
|
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are 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());
|
|
|
|
queryElems(document, '.show-panel', (el) => el.addEventListener('click', onShowPanelClick));
|
|
queryElems(document, '.hide-panel', (el) => el.addEventListener('click', onHidePanelClick));
|
|
queryElems(document, '.show-modal', (el) => el.addEventListener('click', onShowModalClick));
|
|
}
|