Fossil SCM

When approving, rejecting, or changing the status of a forum post using the new controls, disable the associated button when its underlying form is submitted so that the user is not tempted to tap those again. Successful posting redirects the page and a failure will unblock the button after a brief wait.

stephan 2026-07-24 10:45 UTC trunk
Commit e0c33b0519d6000d1b49386ad3b60e1fc28de69805e1da3e50784aef61417724
1 file changed +26 -3
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -990,10 +990,33 @@
990990
state. */
991991
setTimeout(()=>{delete form.dataset.submitted}, 7000);
992992
return;
993993
};
994994
995
+ /**
996
+ The problem: "Approve", "Reject" and their ilk stays active
997
+ while their form post is in flight, just begging to be
998
+ clicked again. We don't really have a way to know when the
999
+ submit finishes, so we'll (A) disable them before submit and
1000
+ (B) reenable them after some long timeout in case the request
1001
+ fails. On a successful submit we're redirected, making this
1002
+ all moot.
1003
+ */
1004
+ const deactivateButtonInFlight = (elem, form)=>{
1005
+ if( elem.disabled ) return /* potential double-click protection */;
1006
+ elem.disabled = true;
1007
+ setTimeout(
1008
+ /* This is a stupid workaround for a failed request but none
1009
+ better come to mind except replacing the form.submit()
1010
+ with a fetch() POST, such that we can do this at
1011
+ precisely the right time. That change would not be
1012
+ difficult (we do this in /chat, /wikiedit, etc.) but
1013
+ that exceeds this morning's ambitions. */
1014
+ ()=>elem.disabled = false, 10000
1015
+ );
1016
+ form.submit();
1017
+ };
9951018
document.querySelectorAll("form").forEach(function(form){
9961019
/* Set up controls for closing posts and setting thread
9971020
status. */
9981021
form.addEventListener('submit', formSubmitted);
9991022
form
@@ -1002,11 +1025,11 @@
10021025
e.classList.remove('hidden');
10031026
F.confirmer(e, {
10041027
confirmText: (e.classList.contains('action-reopen')
10051028
? "Confirm re-open"
10061029
: "Confirm close"),
1007
- onconfirm: ()=>form.submit()
1030
+ onconfirm: ()=>deactivateButtonInFlight(e, form)
10081031
});
10091032
});
10101033
form
10111034
.querySelectorAll("input.action-approve, input.action-reject")
10121035
.forEach(function(e){
@@ -1025,11 +1048,11 @@
10251048
D.input('hidden'),
10261049
'name', isApprove ? 'approve' : 'reject',
10271050
'value', 'ignored'
10281051
)
10291052
);
1030
- form.submit();
1053
+ deactivateButtonInFlight(e, form);
10311054
}
10321055
});
10331056
/* We should also arguably disable the approve/reject
10341057
button's counterpart while it's counting down. */
10351058
});
@@ -1056,11 +1079,11 @@
10561079
};
10571080
sel.addEventListener('change', updateButton, true);
10581081
updateButton();
10591082
F.confirmer(btn, {
10601083
confirmText: "Confirm status change",
1061
- onconfirm: ()=>form.submit()
1084
+ onconfirm: ()=>deactivateButtonInFlight(btn, form)
10621085
});
10631086
});
10641087
});
10651088
}
10661089
10671090
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -990,10 +990,33 @@
990 state. */
991 setTimeout(()=>{delete form.dataset.submitted}, 7000);
992 return;
993 };
994
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
995 document.querySelectorAll("form").forEach(function(form){
996 /* Set up controls for closing posts and setting thread
997 status. */
998 form.addEventListener('submit', formSubmitted);
999 form
@@ -1002,11 +1025,11 @@
1002 e.classList.remove('hidden');
1003 F.confirmer(e, {
1004 confirmText: (e.classList.contains('action-reopen')
1005 ? "Confirm re-open"
1006 : "Confirm close"),
1007 onconfirm: ()=>form.submit()
1008 });
1009 });
1010 form
1011 .querySelectorAll("input.action-approve, input.action-reject")
1012 .forEach(function(e){
@@ -1025,11 +1048,11 @@
1025 D.input('hidden'),
1026 'name', isApprove ? 'approve' : 'reject',
1027 'value', 'ignored'
1028 )
1029 );
1030 form.submit();
1031 }
1032 });
1033 /* We should also arguably disable the approve/reject
1034 button's counterpart while it's counting down. */
1035 });
@@ -1056,11 +1079,11 @@
1056 };
1057 sel.addEventListener('change', updateButton, true);
1058 updateButton();
1059 F.confirmer(btn, {
1060 confirmText: "Confirm status change",
1061 onconfirm: ()=>form.submit()
1062 });
1063 });
1064 });
1065 }
1066
1067
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -990,10 +990,33 @@
990 state. */
991 setTimeout(()=>{delete form.dataset.submitted}, 7000);
992 return;
993 };
994
995 /**
996 The problem: "Approve", "Reject" and their ilk stays active
997 while their form post is in flight, just begging to be
998 clicked again. We don't really have a way to know when the
999 submit finishes, so we'll (A) disable them before submit and
1000 (B) reenable them after some long timeout in case the request
1001 fails. On a successful submit we're redirected, making this
1002 all moot.
1003 */
1004 const deactivateButtonInFlight = (elem, form)=>{
1005 if( elem.disabled ) return /* potential double-click protection */;
1006 elem.disabled = true;
1007 setTimeout(
1008 /* This is a stupid workaround for a failed request but none
1009 better come to mind except replacing the form.submit()
1010 with a fetch() POST, such that we can do this at
1011 precisely the right time. That change would not be
1012 difficult (we do this in /chat, /wikiedit, etc.) but
1013 that exceeds this morning's ambitions. */
1014 ()=>elem.disabled = false, 10000
1015 );
1016 form.submit();
1017 };
1018 document.querySelectorAll("form").forEach(function(form){
1019 /* Set up controls for closing posts and setting thread
1020 status. */
1021 form.addEventListener('submit', formSubmitted);
1022 form
@@ -1002,11 +1025,11 @@
1025 e.classList.remove('hidden');
1026 F.confirmer(e, {
1027 confirmText: (e.classList.contains('action-reopen')
1028 ? "Confirm re-open"
1029 : "Confirm close"),
1030 onconfirm: ()=>deactivateButtonInFlight(e, form)
1031 });
1032 });
1033 form
1034 .querySelectorAll("input.action-approve, input.action-reject")
1035 .forEach(function(e){
@@ -1025,11 +1048,11 @@
1048 D.input('hidden'),
1049 'name', isApprove ? 'approve' : 'reject',
1050 'value', 'ignored'
1051 )
1052 );
1053 deactivateButtonInFlight(e, form);
1054 }
1055 });
1056 /* We should also arguably disable the approve/reject
1057 button's counterpart while it's counting down. */
1058 });
@@ -1056,11 +1079,11 @@
1079 };
1080 sel.addEventListener('change', updateButton, true);
1081 updateButton();
1082 F.confirmer(btn, {
1083 confirmText: "Confirm status change",
1084 onconfirm: ()=>deactivateButtonInFlight(btn, form)
1085 });
1086 });
1087 });
1088 }
1089
1090

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button