-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split common-global.js into separate files (#31438)
To improve maintainability
- Loading branch information
1 parent
ed5ded3
commit 0678287
Showing
10 changed files
with
497 additions
and
484 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import $ from 'jquery'; | ||
import {POST} from '../modules/fetch.js'; | ||
import {hideElem, showElem, toggleElem} from '../utils/dom.js'; | ||
import {showErrorToast} from '../modules/toast.js'; | ||
|
||
export function initGlobalButtonClickOnEnter() { | ||
$(document).on('keypress', 'div.ui.button,span.ui.button', (e) => { | ||
if (e.code === ' ' || e.code === 'Enter') { | ||
$(e.target).trigger('click'); | ||
e.preventDefault(); | ||
} | ||
}); | ||
} | ||
|
||
export function initGlobalDeleteButton() { | ||
// ".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('.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; | ||
} | ||
} | ||
|
||
$(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(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'); | ||
}); | ||
} | ||
} | ||
|
||
export function initGlobalButtons() { | ||
// 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") | ||
$(document).on('click', 'form button.ui.cancel.button', (e) => { | ||
e.preventDefault(); | ||
}); | ||
|
||
$('.show-panel').on('click', function (e) { | ||
// a '.show-panel' element can show a panel, by `data-panel="selector"` | ||
// if it has "toggle" class, it toggles the panel | ||
e.preventDefault(); | ||
const sel = this.getAttribute('data-panel'); | ||
if (this.classList.contains('toggle')) { | ||
toggleElem(sel); | ||
} else { | ||
showElem(sel); | ||
} | ||
}); | ||
|
||
$('.hide-panel').on('click', function (e) { | ||
// a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"` | ||
e.preventDefault(); | ||
let sel = this.getAttribute('data-panel'); | ||
if (sel) { | ||
hideElem($(sel)); | ||
return; | ||
} | ||
sel = this.getAttribute('data-panel-closest'); | ||
if (sel) { | ||
hideElem($(this).closest(sel)); | ||
return; | ||
} | ||
// should never happen, otherwise there is a bug in code | ||
showErrorToast('Nothing to hide'); | ||
}); | ||
} | ||
|
||
export function initGlobalShowModal() { | ||
// 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 '.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. | ||
$('.show-modal').on('click', function (e) { | ||
e.preventDefault(); | ||
const modalSelector = this.getAttribute('data-modal'); | ||
const $modal = $(modalSelector); | ||
if (!$modal.length) { | ||
throw new Error('no modal for this action'); | ||
} | ||
const modalAttrPrefix = 'data-modal-'; | ||
for (const attrib of this.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" -> ".target" -> "target tag" | ||
let $attrTarget = $modal.find(`#${attrTargetName}`); | ||
if (!$attrTarget.length) $attrTarget = $modal.find(`.${attrTargetName}`); | ||
if (!$attrTarget.length) $attrTarget = $modal.find(`${attrTargetName}`); | ||
if (!$attrTarget.length) continue; // TODO: show errors in dev mode to remind developers that there is a bug | ||
|
||
if (attrTargetAttr) { | ||
$attrTarget[0][attrTargetAttr] = attrib.value; | ||
} else if ($attrTarget[0].matches('input, textarea')) { | ||
$attrTarget.val(attrib.value); // FIXME: add more supports like checkbox | ||
} else { | ||
$attrTarget[0].textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p | ||
} | ||
} | ||
|
||
$modal.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 ($modal.find('.form-fetch-action').length) return false; | ||
}, | ||
}).modal('show'); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import {request} from '../modules/fetch.js'; | ||
import {showErrorToast} from '../modules/toast.js'; | ||
import {submitEventSubmitter} from '../utils/dom.js'; | ||
import {htmlEscape} from 'escape-goat'; | ||
import {confirmModal} from './comp/ConfirmModal.js'; | ||
|
||
const {appSubUrl, i18n} = window.config; | ||
|
||
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location" | ||
// more details are in the backend's fetch-redirect handler | ||
function fetchActionDoRedirect(redirect) { | ||
const form = document.createElement('form'); | ||
const input = document.createElement('input'); | ||
form.method = 'post'; | ||
form.action = `${appSubUrl}/-/fetch-redirect`; | ||
input.type = 'hidden'; | ||
input.name = 'redirect'; | ||
input.value = redirect; | ||
form.append(input); | ||
document.body.append(form); | ||
form.submit(); | ||
} | ||
|
||
async function fetchActionDoRequest(actionElem, url, opt) { | ||
try { | ||
const resp = await request(url, opt); | ||
if (resp.status === 200) { | ||
let {redirect} = await resp.json(); | ||
redirect = redirect || actionElem.getAttribute('data-redirect'); | ||
actionElem.classList.remove('dirty'); // remove the areYouSure check before reloading | ||
if (redirect) { | ||
fetchActionDoRedirect(redirect); | ||
} else { | ||
window.location.reload(); | ||
} | ||
return; | ||
} else if (resp.status >= 400 && resp.status < 500) { | ||
const data = await resp.json(); | ||
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" | ||
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond. | ||
if (data.errorMessage) { | ||
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'}); | ||
} else { | ||
showErrorToast(`server error: ${resp.status}`); | ||
} | ||
} else { | ||
showErrorToast(`server error: ${resp.status}`); | ||
} | ||
} catch (e) { | ||
if (e.name !== 'AbortError') { | ||
console.error('error when doRequest', e); | ||
showErrorToast(`${i18n.network_error} ${e}`); | ||
} | ||
} | ||
actionElem.classList.remove('is-loading', 'loading-icon-2px'); | ||
} | ||
|
||
async function formFetchAction(e) { | ||
if (!e.target.classList.contains('form-fetch-action')) return; | ||
|
||
e.preventDefault(); | ||
const formEl = e.target; | ||
if (formEl.classList.contains('is-loading')) return; | ||
|
||
formEl.classList.add('is-loading'); | ||
if (formEl.clientHeight < 50) { | ||
formEl.classList.add('loading-icon-2px'); | ||
} | ||
|
||
const formMethod = formEl.getAttribute('method') || 'get'; | ||
const formActionUrl = formEl.getAttribute('action'); | ||
const formData = new FormData(formEl); | ||
const formSubmitter = submitEventSubmitter(e); | ||
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')]; | ||
if (submitterName) { | ||
formData.append(submitterName, submitterValue || ''); | ||
} | ||
|
||
let reqUrl = formActionUrl; | ||
const reqOpt = {method: formMethod.toUpperCase()}; | ||
if (formMethod.toLowerCase() === 'get') { | ||
const params = new URLSearchParams(); | ||
for (const [key, value] of formData) { | ||
params.append(key, value.toString()); | ||
} | ||
const pos = reqUrl.indexOf('?'); | ||
if (pos !== -1) { | ||
reqUrl = reqUrl.slice(0, pos); | ||
} | ||
reqUrl += `?${params.toString()}`; | ||
} else { | ||
reqOpt.body = formData; | ||
} | ||
|
||
await fetchActionDoRequest(formEl, reqUrl, reqOpt); | ||
} | ||
|
||
async function linkAction(e) { | ||
// A "link-action" can post AJAX request to its "data-url" | ||
// Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading. | ||
// If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action. | ||
const el = e.target.closest('.link-action'); | ||
if (!el) return; | ||
|
||
e.preventDefault(); | ||
const url = el.getAttribute('data-url'); | ||
const doRequest = async () => { | ||
el.disabled = true; | ||
await fetchActionDoRequest(el, url, {method: 'POST'}); | ||
el.disabled = false; | ||
}; | ||
|
||
const modalConfirmContent = htmlEscape(el.getAttribute('data-modal-confirm') || ''); | ||
if (!modalConfirmContent) { | ||
await doRequest(); | ||
return; | ||
} | ||
|
||
const isRisky = el.classList.contains('red') || el.classList.contains('negative'); | ||
if (await confirmModal(modalConfirmContent, {confirmButtonColor: isRisky ? 'red' : 'primary'})) { | ||
await doRequest(); | ||
} | ||
} | ||
|
||
export function initGlobalFetchAction() { | ||
document.addEventListener('submit', formFetchAction); | ||
document.addEventListener('click', linkAction); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import $ from 'jquery'; | ||
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js'; | ||
|
||
export function initGlobalFormDirtyLeaveConfirm() { | ||
// Warn users that try to leave a page after entering data into a form. | ||
// Except on sign-in pages, and for forms marked as 'ignore-dirty'. | ||
if (!$('.user.signin').length) { | ||
$('form:not(.ignore-dirty)').areYouSure(); | ||
} | ||
} | ||
|
||
export function initGlobalEnterQuickSubmit() { | ||
document.addEventListener('keydown', (e) => { | ||
if (e.key !== 'Enter') return; | ||
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); | ||
if (hasCtrlOrMeta && e.target.matches('textarea')) { | ||
if (handleGlobalEnterQuickSubmit(e.target)) { | ||
e.preventDefault(); | ||
} | ||
} else if (e.target.matches('input') && !e.target.closest('form')) { | ||
// input in a normal form could handle Enter key by default, so we only handle the input outside a form | ||
// eslint-disable-next-line unicorn/no-lonely-if | ||
if (handleGlobalEnterQuickSubmit(e.target)) { | ||
e.preventDefault(); | ||
} | ||
} | ||
}); | ||
} |
Oops, something went wrong.