Changement du refresh handler, soft reset d'ajouter sur beaucoup de formulaire de modification
CI - Security, Lint & Tests / validate (push) Failing after 1m3s

This commit is contained in:
cedrick2711
2026-09-08 11:27:53 -04:00
parent e2e2b1b07e
commit 4a2e2f053f
42 changed files with 722 additions and 333 deletions
+193 -16
View File
@@ -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 = '&times;';
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;