Changement du refresh handler, soft reset d'ajouter sur beaucoup de formulaire de modification
CI - Security, Lint & Tests / validate (push) Failing after 1m3s
CI - Security, Lint & Tests / validate (push) Failing after 1m3s
This commit is contained in:
+193
-16
@@ -420,22 +420,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
initDraggableBlocks();
|
||||
|
||||
/**
|
||||
* Auto-dismiss flash alerts after 5 seconds.
|
||||
* Auto-dismiss flash alerts after 5 seconds (see armAlertAutoDismiss).
|
||||
*/
|
||||
const alerts = document.querySelectorAll('.alert-dismissible');
|
||||
alerts.forEach(function(alert) {
|
||||
setTimeout(function() {
|
||||
if (alert.parentElement) {
|
||||
alert.style.opacity = '0';
|
||||
alert.style.transition = 'opacity 0.3s ease';
|
||||
setTimeout(function() {
|
||||
if (alert.parentElement) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
armAlertAutoDismiss();
|
||||
});
|
||||
/* =========================================================================
|
||||
Declarative behaviours — replacing inline event handlers
|
||||
@@ -593,7 +580,7 @@ document.addEventListener('input', function (event) {
|
||||
document.addEventListener('change', function (event) {
|
||||
const control = event.target.closest('[data-submit-on-change]');
|
||||
if (control && control.form) {
|
||||
control.form.submit();
|
||||
submitForm(control.form);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -613,3 +600,193 @@ document.addEventListener('change', function (event) {
|
||||
window.location.href = select.getAttribute('data-navigate')
|
||||
.replace('{value}', encodeURIComponent(select.value));
|
||||
});
|
||||
|
||||
/* =========================================================================
|
||||
AJAX forms — server-rendered changes without a full page reload
|
||||
=========================================================================
|
||||
|
||||
A <form method="post" data-ajax> is submitted with fetch() instead of a
|
||||
navigation. The route answers a JSON payload (see app/responders.py) that
|
||||
says where to fetch the refreshed content and which flash messages to
|
||||
show; the content region is then swapped in place, so scroll position and
|
||||
open state survive between edits.
|
||||
|
||||
A stale edit — the page changed since this copy was loaded — is answered
|
||||
409, and the user is asked to reload rather than silently overwrite the
|
||||
other person's change.
|
||||
========================================================================= */
|
||||
|
||||
let TT_REVISION = null;
|
||||
|
||||
function armAlertAutoDismiss() {
|
||||
const alerts = document.querySelectorAll('.alert-dismissible');
|
||||
alerts.forEach(function (alert) {
|
||||
if (alert.getAttribute('data-armed')) return;
|
||||
alert.setAttribute('data-armed', '1');
|
||||
setTimeout(function () {
|
||||
if (alert.parentElement) {
|
||||
alert.style.opacity = '0';
|
||||
alert.style.transition = 'opacity 0.3s ease';
|
||||
setTimeout(function () {
|
||||
if (alert.parentElement) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchRevision() {
|
||||
return fetch('/api/revision', {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
credentials: 'same-origin'
|
||||
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||
TT_REVISION = data.revision;
|
||||
return data.revision;
|
||||
}).catch(function () {
|
||||
// Revision tracking is best-effort; a failed lookup just disables it.
|
||||
});
|
||||
}
|
||||
|
||||
function showFlashMessages(messages) {
|
||||
const container = document.querySelector('.flash-messages');
|
||||
if (!container || !messages || !messages.length) return;
|
||||
messages.forEach(function (m) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-' + (m.category || 'info') + ' alert-dismissible';
|
||||
const span = document.createElement('span');
|
||||
span.textContent = m.text;
|
||||
const close = document.createElement('button');
|
||||
close.type = 'button';
|
||||
close.className = 'alert-close';
|
||||
close.setAttribute('data-action', 'dismiss-alert');
|
||||
close.setAttribute('aria-label', 'Dismiss');
|
||||
close.innerHTML = '×';
|
||||
alert.appendChild(span);
|
||||
alert.appendChild(close);
|
||||
container.appendChild(alert);
|
||||
});
|
||||
armAlertAutoDismiss();
|
||||
}
|
||||
|
||||
function replaceContent(html, url) {
|
||||
const holder = document.createElement('div');
|
||||
holder.innerHTML = html;
|
||||
const next = holder.querySelector('#page-content');
|
||||
const target = document.getElementById('page-content');
|
||||
if (next && target) {
|
||||
target.innerHTML = next.innerHTML;
|
||||
}
|
||||
if (url && window.history && window.history.replaceState) {
|
||||
window.history.replaceState(null, '', url);
|
||||
}
|
||||
initDraggableBlocks();
|
||||
document.dispatchEvent(new Event('fragment:updated'));
|
||||
}
|
||||
|
||||
function refreshPage(url) {
|
||||
return fetch(url, { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.text(); })
|
||||
.then(function (html) { replaceContent(html, url); });
|
||||
}
|
||||
|
||||
function submitAjaxForm(form) {
|
||||
const headers = {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
const token = form.querySelector('input[name="csrf_token"]');
|
||||
if (token) headers['X-CSRFToken'] = token.value;
|
||||
if (TT_REVISION !== null && TT_REVISION !== undefined) {
|
||||
headers['X-Revision'] = String(TT_REVISION);
|
||||
}
|
||||
|
||||
fetch(form.action, {
|
||||
method: (form.method || 'POST').toUpperCase(),
|
||||
body: new FormData(form),
|
||||
headers: headers,
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
return response.json().then(function (data) {
|
||||
return { status: response.status, data: data };
|
||||
}).catch(function () {
|
||||
return { status: response.status, data: null };
|
||||
});
|
||||
}).then(function (result) {
|
||||
if (result.status === 409) {
|
||||
const message = (result.data && result.data.message) ||
|
||||
'This page was changed by someone else. Reload to see the latest changes.';
|
||||
if (window.confirm(message)) {
|
||||
window.location.reload();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (result.status >= 400) {
|
||||
const message = (result.data && (result.data.message || result.data.error)) ||
|
||||
'Something went wrong. Please try again.';
|
||||
window.alert(message);
|
||||
return null;
|
||||
}
|
||||
return result.data;
|
||||
}).then(function (data) {
|
||||
if (!data) return;
|
||||
if (data.revision !== undefined && data.revision !== null) {
|
||||
TT_REVISION = data.revision;
|
||||
}
|
||||
showFlashMessages(data.messages);
|
||||
if (data.redirect) {
|
||||
return refreshPage(data.redirect);
|
||||
}
|
||||
return undefined;
|
||||
}).catch(function (err) {
|
||||
console.error('AJAX form submission failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function submitForm(form) {
|
||||
if (form.hasAttribute('data-ajax')) {
|
||||
submitAjaxForm(form);
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
function checkForRemoteChanges() {
|
||||
return fetchRevision().then(function (rev) {
|
||||
if (TT_REVISION !== null && rev !== TT_REVISION) {
|
||||
TT_REVISION = rev;
|
||||
return refreshPage(window.location.href);
|
||||
}
|
||||
return undefined;
|
||||
}).catch(function () { /* best-effort */ });
|
||||
}
|
||||
|
||||
// data-submit-on-change and data-ajax both funnel through submitForm so a
|
||||
// <select> change uses fetch() when the form is marked, and a plain submit
|
||||
// otherwise. (form.submit() does not fire the submit event, so intercepting
|
||||
// only 'submit' would miss the selects.)
|
||||
document.addEventListener('submit', function (event) {
|
||||
const form = event.target.closest('form[data-ajax]');
|
||||
if (!form || event.defaultPrevented) return;
|
||||
event.preventDefault();
|
||||
submitAjaxForm(form);
|
||||
});
|
||||
|
||||
// Refresh when the user returns to the tab: if someone else changed the data,
|
||||
// this brings the view up to date without a constant polling loop.
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkForRemoteChanges();
|
||||
}
|
||||
});
|
||||
window.addEventListener('focus', function () {
|
||||
checkForRemoteChanges();
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
fetchRevision();
|
||||
});
|
||||
|
||||
window.TT = window.TT || {};
|
||||
window.TT.refreshPage = refreshPage;
|
||||
Reference in New Issue
Block a user