// pgv3-forms.js — site-wide submit-button lock to prevent double-submits.
//
// Closes UX blocker U5 (docs/plans/2026-04-14-ux-reaudit.md): before this,
// double-clicking a Create / Save button could send two POSTs, producing
// duplicate records (or two conflicting writes on slow networks).
//
// Behaviour
// =========
// On DOMContentLoaded:
// - Every <form> in the document gets a single 'submit' listener.
// - When a form submits, every <button type="submit"> and <input
// type="submit"> inside that form is disabled and given class
// 'pgv3-submitting'. The original label is stashed on dataset.origLabel
// and replaced with the button's data-busy-label (default "…").
// - If the submit handler returned falsy (preventDefault'd by an
// AJAX form, e.g. /profile), the buttons are re-enabled when the
// form dispatches a custom 'pgv3:done' event OR after a fallback
// timeout (10s) so a failed AJAX submit doesn't strand the form.
// - For traditional submits (default behaviour — browser navigates),
// the buttons stay disabled because the page is about to unload.
//
// AJAX forms that preventDefault should dispatch `pgv3:done` on the
// form element when they finish, success or failure. The profile
// form's pgv3SaveProfile() is updated in the same slice.
//
// CSS: add styling for .pgv3-submitting in your stylesheet (or rely on
// the default disabled-button styling — still better than unchanged).
(function () {
'use strict'
var FALLBACK_TIMEOUT_MS = 10000
function lockForm (form) {
var controls = form.querySelectorAll('button[type="submit"], input[type="submit"], button:not([type])')
controls.forEach(function (btn) {
if (btn.dataset.pgv3Locked) return
btn.dataset.pgv3Locked = '1'
btn.dataset.pgv3OrigLabel = btn.textContent || btn.value || ''
var busy = btn.dataset.busyLabel || 'Working…'
if (btn.tagName === 'BUTTON') btn.textContent = busy
else btn.value = busy
btn.disabled = true
btn.classList.add('pgv3-submitting')
})
}
function unlockForm (form) {
var controls = form.querySelectorAll('button[type="submit"], input[type="submit"], button:not([type])')
controls.forEach(function (btn) {
if (!btn.dataset.pgv3Locked) return
btn.disabled = false
btn.classList.remove('pgv3-submitting')
var orig = btn.dataset.pgv3OrigLabel
if (orig != null) {
if (btn.tagName === 'BUTTON') btn.textContent = orig
else btn.value = orig
}
delete btn.dataset.pgv3Locked
delete btn.dataset.pgv3OrigLabel
})
}
function attachLock (form) {
if (form.dataset.pgv3FormsBound) return
form.dataset.pgv3FormsBound = '1'
form.addEventListener('submit', function (ev) {
// Lock immediately, before the event default runs. If the form
// uses preventDefault (AJAX), we'll rely on pgv3:done or the
// fallback timer to unlock. If it doesn't, the browser navigates
// and the new page reinitialises everything.
lockForm(form)
if (ev.defaultPrevented || form.dataset.pgv3Ajax === '1') {
// AJAX form — wait for explicit done or timeout.
var fallback = setTimeout(function () {
unlockForm(form)
}, FALLBACK_TIMEOUT_MS)
form.addEventListener('pgv3:done', function onDone () {
clearTimeout(fallback)
form.removeEventListener('pgv3:done', onDone)
unlockForm(form)
})
}
})
}
function initAll () {
document.querySelectorAll('form').forEach(attachLock)
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll)
} else {
initAll()
}
// Expose a tiny API so inline handlers / future dynamic forms can
// participate without re-implementing the locking logic.
window.pgv3Forms = { lock: lockForm, unlock: unlockForm, attach: attachLock }
})()