Skip to content

Commit 0678287

Browse files
authored
Split common-global.js into separate files (#31438)
To improve maintainability
1 parent ed5ded3 commit 0678287

10 files changed

+497
-484
lines changed

web_src/js/features/admin/common.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import $ from 'jquery';
2-
import {checkAppUrl} from '../common-global.js';
2+
import {checkAppUrl} from '../common-page.js';
33
import {hideElem, showElem, toggleElem} from '../../utils/dom.js';
44
import {POST} from '../../modules/fetch.js';
55

web_src/js/features/common-button.js

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import $ from 'jquery';
2+
import {POST} from '../modules/fetch.js';
3+
import {hideElem, showElem, toggleElem} from '../utils/dom.js';
4+
import {showErrorToast} from '../modules/toast.js';
5+
6+
export function initGlobalButtonClickOnEnter() {
7+
$(document).on('keypress', 'div.ui.button,span.ui.button', (e) => {
8+
if (e.code === ' ' || e.code === 'Enter') {
9+
$(e.target).trigger('click');
10+
e.preventDefault();
11+
}
12+
});
13+
}
14+
15+
export function initGlobalDeleteButton() {
16+
// ".delete-button" shows a confirmation modal defined by `data-modal-id` attribute.
17+
// Some model/form elements will be filled by `data-id` / `data-name` / `data-data-xxx` attributes.
18+
// If there is a form defined by `data-form`, then the form will be submitted as-is (without any modification).
19+
// If there is no form, then the data will be posted to `data-url`.
20+
// TODO: it's not encouraged to use this method. `show-modal` does far better than this.
21+
for (const btn of document.querySelectorAll('.delete-button')) {
22+
btn.addEventListener('click', (e) => {
23+
e.preventDefault();
24+
25+
// eslint-disable-next-line github/no-dataset -- code depends on the camel-casing
26+
const dataObj = btn.dataset;
27+
28+
const modalId = btn.getAttribute('data-modal-id');
29+
const modal = document.querySelector(`.delete.modal${modalId ? `#${modalId}` : ''}`);
30+
31+
// set the modal "display name" by `data-name`
32+
const modalNameEl = modal.querySelector('.name');
33+
if (modalNameEl) modalNameEl.textContent = btn.getAttribute('data-name');
34+
35+
// fill the modal elements with data-xxx attributes: `data-data-organization-name="..."` => `<span class="dataOrganizationName">...</span>`
36+
for (const [key, value] of Object.entries(dataObj)) {
37+
if (key.startsWith('data')) {
38+
const textEl = modal.querySelector(`.${key}`);
39+
if (textEl) textEl.textContent = value;
40+
}
41+
}
42+
43+
$(modal).modal({
44+
closable: false,
45+
onApprove: async () => {
46+
// if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."`
47+
if (btn.getAttribute('data-type') === 'form') {
48+
const formSelector = btn.getAttribute('data-form');
49+
const form = document.querySelector(formSelector);
50+
if (!form) throw new Error(`no form named ${formSelector} found`);
51+
form.submit();
52+
}
53+
54+
// prepare an AJAX form by data attributes
55+
const postData = new FormData();
56+
for (const [key, value] of Object.entries(dataObj)) {
57+
if (key.startsWith('data')) { // for data-data-xxx (HTML) -> dataXxx (form)
58+
postData.append(key.slice(4), value);
59+
}
60+
if (key === 'id') { // for data-id="..."
61+
postData.append('id', value);
62+
}
63+
}
64+
65+
const response = await POST(btn.getAttribute('data-url'), {data: postData});
66+
if (response.ok) {
67+
const data = await response.json();
68+
window.location.href = data.redirect;
69+
}
70+
},
71+
}).modal('show');
72+
});
73+
}
74+
}
75+
76+
export function initGlobalButtons() {
77+
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
78+
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
79+
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
80+
$(document).on('click', 'form button.ui.cancel.button', (e) => {
81+
e.preventDefault();
82+
});
83+
84+
$('.show-panel').on('click', function (e) {
85+
// a '.show-panel' element can show a panel, by `data-panel="selector"`
86+
// if it has "toggle" class, it toggles the panel
87+
e.preventDefault();
88+
const sel = this.getAttribute('data-panel');
89+
if (this.classList.contains('toggle')) {
90+
toggleElem(sel);
91+
} else {
92+
showElem(sel);
93+
}
94+
});
95+
96+
$('.hide-panel').on('click', function (e) {
97+
// a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
98+
e.preventDefault();
99+
let sel = this.getAttribute('data-panel');
100+
if (sel) {
101+
hideElem($(sel));
102+
return;
103+
}
104+
sel = this.getAttribute('data-panel-closest');
105+
if (sel) {
106+
hideElem($(this).closest(sel));
107+
return;
108+
}
109+
// should never happen, otherwise there is a bug in code
110+
showErrorToast('Nothing to hide');
111+
});
112+
}
113+
114+
export function initGlobalShowModal() {
115+
// A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
116+
// Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
117+
// * First, try to query '#target'
118+
// * Then, try to query '.target'
119+
// * Then, try to query 'target' as HTML tag
120+
// If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
121+
$('.show-modal').on('click', function (e) {
122+
e.preventDefault();
123+
const modalSelector = this.getAttribute('data-modal');
124+
const $modal = $(modalSelector);
125+
if (!$modal.length) {
126+
throw new Error('no modal for this action');
127+
}
128+
const modalAttrPrefix = 'data-modal-';
129+
for (const attrib of this.attributes) {
130+
if (!attrib.name.startsWith(modalAttrPrefix)) {
131+
continue;
132+
}
133+
134+
const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length);
135+
const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.');
136+
// try to find target by: "#target" -> ".target" -> "target tag"
137+
let $attrTarget = $modal.find(`#${attrTargetName}`);
138+
if (!$attrTarget.length) $attrTarget = $modal.find(`.${attrTargetName}`);
139+
if (!$attrTarget.length) $attrTarget = $modal.find(`${attrTargetName}`);
140+
if (!$attrTarget.length) continue; // TODO: show errors in dev mode to remind developers that there is a bug
141+
142+
if (attrTargetAttr) {
143+
$attrTarget[0][attrTargetAttr] = attrib.value;
144+
} else if ($attrTarget[0].matches('input, textarea')) {
145+
$attrTarget.val(attrib.value); // FIXME: add more supports like checkbox
146+
} else {
147+
$attrTarget[0].textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p
148+
}
149+
}
150+
151+
$modal.modal('setting', {
152+
onApprove: () => {
153+
// "form-fetch-action" can handle network errors gracefully,
154+
// so keep the modal dialog to make users can re-submit the form if anything wrong happens.
155+
if ($modal.find('.form-fetch-action').length) return false;
156+
},
157+
}).modal('show');
158+
});
159+
}
+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {request} from '../modules/fetch.js';
2+
import {showErrorToast} from '../modules/toast.js';
3+
import {submitEventSubmitter} from '../utils/dom.js';
4+
import {htmlEscape} from 'escape-goat';
5+
import {confirmModal} from './comp/ConfirmModal.js';
6+
7+
const {appSubUrl, i18n} = window.config;
8+
9+
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
10+
// more details are in the backend's fetch-redirect handler
11+
function fetchActionDoRedirect(redirect) {
12+
const form = document.createElement('form');
13+
const input = document.createElement('input');
14+
form.method = 'post';
15+
form.action = `${appSubUrl}/-/fetch-redirect`;
16+
input.type = 'hidden';
17+
input.name = 'redirect';
18+
input.value = redirect;
19+
form.append(input);
20+
document.body.append(form);
21+
form.submit();
22+
}
23+
24+
async function fetchActionDoRequest(actionElem, url, opt) {
25+
try {
26+
const resp = await request(url, opt);
27+
if (resp.status === 200) {
28+
let {redirect} = await resp.json();
29+
redirect = redirect || actionElem.getAttribute('data-redirect');
30+
actionElem.classList.remove('dirty'); // remove the areYouSure check before reloading
31+
if (redirect) {
32+
fetchActionDoRedirect(redirect);
33+
} else {
34+
window.location.reload();
35+
}
36+
return;
37+
} else if (resp.status >= 400 && resp.status < 500) {
38+
const data = await resp.json();
39+
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
40+
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
41+
if (data.errorMessage) {
42+
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
43+
} else {
44+
showErrorToast(`server error: ${resp.status}`);
45+
}
46+
} else {
47+
showErrorToast(`server error: ${resp.status}`);
48+
}
49+
} catch (e) {
50+
if (e.name !== 'AbortError') {
51+
console.error('error when doRequest', e);
52+
showErrorToast(`${i18n.network_error} ${e}`);
53+
}
54+
}
55+
actionElem.classList.remove('is-loading', 'loading-icon-2px');
56+
}
57+
58+
async function formFetchAction(e) {
59+
if (!e.target.classList.contains('form-fetch-action')) return;
60+
61+
e.preventDefault();
62+
const formEl = e.target;
63+
if (formEl.classList.contains('is-loading')) return;
64+
65+
formEl.classList.add('is-loading');
66+
if (formEl.clientHeight < 50) {
67+
formEl.classList.add('loading-icon-2px');
68+
}
69+
70+
const formMethod = formEl.getAttribute('method') || 'get';
71+
const formActionUrl = formEl.getAttribute('action');
72+
const formData = new FormData(formEl);
73+
const formSubmitter = submitEventSubmitter(e);
74+
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
75+
if (submitterName) {
76+
formData.append(submitterName, submitterValue || '');
77+
}
78+
79+
let reqUrl = formActionUrl;
80+
const reqOpt = {method: formMethod.toUpperCase()};
81+
if (formMethod.toLowerCase() === 'get') {
82+
const params = new URLSearchParams();
83+
for (const [key, value] of formData) {
84+
params.append(key, value.toString());
85+
}
86+
const pos = reqUrl.indexOf('?');
87+
if (pos !== -1) {
88+
reqUrl = reqUrl.slice(0, pos);
89+
}
90+
reqUrl += `?${params.toString()}`;
91+
} else {
92+
reqOpt.body = formData;
93+
}
94+
95+
await fetchActionDoRequest(formEl, reqUrl, reqOpt);
96+
}
97+
98+
async function linkAction(e) {
99+
// A "link-action" can post AJAX request to its "data-url"
100+
// Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
101+
// If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action.
102+
const el = e.target.closest('.link-action');
103+
if (!el) return;
104+
105+
e.preventDefault();
106+
const url = el.getAttribute('data-url');
107+
const doRequest = async () => {
108+
el.disabled = true;
109+
await fetchActionDoRequest(el, url, {method: 'POST'});
110+
el.disabled = false;
111+
};
112+
113+
const modalConfirmContent = htmlEscape(el.getAttribute('data-modal-confirm') || '');
114+
if (!modalConfirmContent) {
115+
await doRequest();
116+
return;
117+
}
118+
119+
const isRisky = el.classList.contains('red') || el.classList.contains('negative');
120+
if (await confirmModal(modalConfirmContent, {confirmButtonColor: isRisky ? 'red' : 'primary'})) {
121+
await doRequest();
122+
}
123+
}
124+
125+
export function initGlobalFetchAction() {
126+
document.addEventListener('submit', formFetchAction);
127+
document.addEventListener('click', linkAction);
128+
}

web_src/js/features/common-form.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import $ from 'jquery';
2+
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
3+
4+
export function initGlobalFormDirtyLeaveConfirm() {
5+
// Warn users that try to leave a page after entering data into a form.
6+
// Except on sign-in pages, and for forms marked as 'ignore-dirty'.
7+
if (!$('.user.signin').length) {
8+
$('form:not(.ignore-dirty)').areYouSure();
9+
}
10+
}
11+
12+
export function initGlobalEnterQuickSubmit() {
13+
document.addEventListener('keydown', (e) => {
14+
if (e.key !== 'Enter') return;
15+
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
16+
if (hasCtrlOrMeta && e.target.matches('textarea')) {
17+
if (handleGlobalEnterQuickSubmit(e.target)) {
18+
e.preventDefault();
19+
}
20+
} else if (e.target.matches('input') && !e.target.closest('form')) {
21+
// input in a normal form could handle Enter key by default, so we only handle the input outside a form
22+
// eslint-disable-next-line unicorn/no-lonely-if
23+
if (handleGlobalEnterQuickSubmit(e.target)) {
24+
e.preventDefault();
25+
}
26+
}
27+
});
28+
}

0 commit comments

Comments
 (0)