aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
Diffstat (limited to 'web_src/js')
-rw-r--r--web_src/js/features/common-global.js84
-rw-r--r--web_src/js/features/comp/QuickSubmit.js21
-rw-r--r--web_src/js/features/repo-code.js2
-rw-r--r--web_src/js/modules/tippy.js14
4 files changed, 107 insertions, 14 deletions
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index b1d3fa22d8..c0e66be51c 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -7,6 +7,7 @@ import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
import {svg} from '../svg.js';
import {hideElem, showElem, toggleElem} from '../utils/dom.js';
import {htmlEscape} from 'escape-goat';
+import {createTippy} from '../modules/tippy.js';
const {appUrl, csrfToken, i18n} = window.config;
@@ -60,6 +61,81 @@ export function initGlobalButtonClickOnEnter() {
});
}
+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('small-loading-icon');
+ }
+
+ const formMethod = formEl.getAttribute('method') || 'get';
+ const formActionUrl = formEl.getAttribute('action');
+ const formData = new FormData(formEl);
+ const [submitterName, submitterValue] = [e.submitter?.getAttribute('name'), e.submitter?.getAttribute('value')];
+ if (submitterName) {
+ formData.append(submitterName, submitterValue || '');
+ }
+
+ let reqUrl = formActionUrl;
+ const reqOpt = {method: formMethod.toUpperCase(), headers: {'X-Csrf-Token': csrfToken}};
+ 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;
+ }
+
+ let errorTippy;
+ const onError = (msg) => {
+ formEl.classList.remove('is-loading', 'small-loading-icon');
+ if (errorTippy) errorTippy.destroy();
+ errorTippy = createTippy(formEl, {
+ content: msg,
+ interactive: true,
+ showOnCreate: true,
+ hideOnClick: true,
+ role: 'alert',
+ theme: 'form-fetch-error',
+ trigger: 'manual',
+ arrow: false,
+ });
+ };
+
+ const doRequest = async () => {
+ try {
+ const resp = await fetch(reqUrl, reqOpt);
+ if (resp.status === 200) {
+ const {redirect} = await resp.json();
+ formEl.classList.remove('dirty'); // remove the areYouSure check before reloading
+ if (redirect) {
+ window.location.href = redirect;
+ } else {
+ window.location.reload();
+ }
+ } else {
+ onError(`server error: ${resp.status}`);
+ }
+ } catch (e) {
+ onError(e.error);
+ }
+ };
+
+ // TODO: add "confirm" support like "link-action" in the future
+ await doRequest();
+}
+
export function initGlobalCommon() {
// Semantic UI modules.
const $uiDropdowns = $('.ui.dropdown');
@@ -114,6 +190,8 @@ export function initGlobalCommon() {
if (btn.classList.contains('loading')) return e.preventDefault();
btn.classList.add('loading');
});
+
+ document.addEventListener('submit', formFetchAction);
}
export function initGlobalDropzone() {
@@ -182,7 +260,7 @@ function linkAction(e) {
const $this = $(e.target);
const redirect = $this.attr('data-redirect');
- const request = () => {
+ const doRequest = () => {
$this.prop('disabled', true);
$.post($this.attr('data-url'), {
_csrf: csrfToken
@@ -201,7 +279,7 @@ function linkAction(e) {
const modalConfirmHtml = htmlEscape($this.attr('data-modal-confirm') || '');
if (!modalConfirmHtml) {
- request();
+ doRequest();
return;
}
@@ -220,7 +298,7 @@ function linkAction(e) {
$modal.appendTo(document.body);
$modal.modal({
onApprove() {
- request();
+ doRequest();
},
onHidden() {
$modal.remove();
diff --git a/web_src/js/features/comp/QuickSubmit.js b/web_src/js/features/comp/QuickSubmit.js
index d598a59655..2587375a71 100644
--- a/web_src/js/features/comp/QuickSubmit.js
+++ b/web_src/js/features/comp/QuickSubmit.js
@@ -1,17 +1,24 @@
import $ from 'jquery';
export function handleGlobalEnterQuickSubmit(target) {
- const $target = $(target);
- const $form = $(target).closest('form');
- if ($form.length) {
+ const form = target.closest('form');
+ if (form) {
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return;
+ }
+
+ if (form.classList.contains('form-fetch-action')) {
+ form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
+ return;
+ }
+
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
- if ($form[0].checkValidity()) {
- $form.trigger('submit');
- }
+ $(form).trigger('submit');
} else {
// if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
// the 'ce-' prefix means this is a CustomEvent
- $target.trigger('ce-quick-submit');
+ target.dispatchEvent(new CustomEvent('ce-quick-submit', {bubbles: true}));
}
}
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index 6a01a8445b..306f38829f 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -111,7 +111,7 @@ function showLineButton() {
hideOnClick: true,
content: menu,
placement: 'right-start',
- interactive: 'true',
+ interactive: true,
onShow: (tippy) => {
tippy.popper.addEventListener('click', () => {
tippy.hide();
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index b424cdfd50..3409e1c714 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -3,6 +3,11 @@ import tippy from 'tippy.js';
const visibleInstances = new Set();
export function createTippy(target, opts = {}) {
+ const {role, content, onHide: optsOnHide, onDestroy: optsOnDestroy, onShow: optOnShow} = opts;
+ delete opts.onHide;
+ delete opts.onDestroy;
+ delete opts.onShow;
+
const instance = tippy(target, {
appendTo: document.body,
animation: false,
@@ -13,9 +18,11 @@ export function createTippy(target, opts = {}) {
maxWidth: 500, // increase over default 350px
onHide: (instance) => {
visibleInstances.delete(instance);
+ return optsOnHide?.(instance);
},
onDestroy: (instance) => {
visibleInstances.delete(instance);
+ return optsOnDestroy?.(instance);
},
onShow: (instance) => {
// hide other tooltip instances so only one tooltip shows at a time
@@ -25,18 +32,19 @@ export function createTippy(target, opts = {}) {
}
}
visibleInstances.add(instance);
+ return optOnShow?.(instance);
},
arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`,
role: 'menu', // HTML role attribute, only tooltips should use "tooltip"
- theme: opts.role || 'menu', // CSS theme, we support either "tooltip" or "menu"
+ theme: role || 'menu', // CSS theme, we support either "tooltip" or "menu"
...opts,
});
// for popups where content refers to a DOM element, we use the 'tippy-target' class
// to initially hide the content, now we can remove it as the content has been removed
// from the DOM by tippy
- if (opts.content instanceof Element) {
- opts.content.classList.remove('tippy-target');
+ if (content instanceof Element) {
+ content.classList.remove('tippy-target');
}
return instance;