Fossil SCM

Add pieces which, when loading a forum thread, visibly tag Reply and Edit buttons for posts which have local draft replies/edits, but do not yet enable them. Still to solve is how to update those tags dynamically as drafts are added and removed within the current page context. They're currently useful, but the lack of dynamic updates to them is confusing.

stephan 2026-06-09 21:55 UTC forum-editor-2026
Commit bee75fb8b22ff892857e4762ea35274fc427e92cd60a3917a457cc96fc876f98
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -43,11 +43,11 @@
4343
submit.
4444
4545
opt.ondiscard[=function]: if set, a Discard button is added
4646
which, when activated, clears the current draft and removes
4747
this object's widget from the DOM. After doing so,
48
- opt.ondiscard() is called and passed no arguments. Exceptions
48
+ opt.ondiscard() is called and passed this object. Exceptions
4949
thrown by ondiscard() are ignored but may be logged.
5050
5151
opt.onsubmit[=function]: if set, this function is called
5252
immediately after the post has been successfully saved, and
5353
passed this object and a JSON-format response object from the
@@ -401,11 +401,11 @@
401401
** before the drafts are cleared and any exceptions it throws are
402402
** ignored (but may be logged).
403403
*/
404404
discard(){
405405
if( this.#opt.ondiscard instanceof Function ){
406
- try{this.#opt.ondiscard();}
406
+ try{this.#opt.ondiscard(this);}
407407
catch(e){
408408
console.error("ForumPostEditor.ondiscard() threw:",e);
409409
}
410410
}
411411
this.#clearDraft();
@@ -981,10 +981,14 @@
981981
j = F.nu(j);
982982
if( j.error ) throw new Error(j.error);
983983
return j;
984984
});
985985
};
986
+
987
+ const makeDraftKey = (prefix,uuid)=>{
988
+ return prefix+'-'+uuid.substr(0,12);
989
+ };
986990
987991
const setupEditReplyElement = (ePost, eButton, eToDisable)=>{
988992
const fpid = ePost.dataset.fpid;
989993
ePost.dataset.originalMarginLeft = ePost.style.marginLeft;
990994
ePost.style.marginLeft = 'initial';
@@ -1011,13 +1015,15 @@
10111015
eBtnReply.innerText = "Replying...";
10121016
const ondone = (fpe, response)=>{
10131017
/* onsubmit() and ondiscard() callback */
10141018
restoreEditReplyElement(ePost, eBtnReply, eToDisable);
10151019
//console.debug("ondiscard/onsubmit", fpe, artifact);
1016
- if( response/*onsubmit*/ ){
1020
+ if( response/*onsubmit()*/ ){
10171021
window.location = F.repoUrl('forumpost/'+response.uuid);
10181022
fpe.close();
1023
+ }else{/*ondiscard()*/
1024
+ eBtnReply.classList.remove('draft');
10191025
}
10201026
};
10211027
const fpe = new F.ForumPostEditor({
10221028
hiddenFields: form.querySelectorAll(
10231029
'input[type=hidden][name=csrf]'
@@ -1025,19 +1031,18 @@
10251031
an edit to that post rather than a response. */
10261032
),
10271033
ondiscard: ondone,
10281034
onsubmit: ondone,
10291035
inReplyTo: fpid,
1030
- draftKey: 'draft-reply-'+(
1031
- fEditHead
1036
+ draftKey: makeDraftKey('draft-reply', fEditHead
10321037
/* The problem with firt as a key is that firt is not
10331038
necessarily the root edit of that post, which is
10341039
what we really want as a draft key so that the
10351040
draft does not disapper if firt is later edited
10361041
(giving us a new firt value here). */
10371042
|| fpid
1038
- ).substr(0,12)
1043
+ )
10391044
});
10401045
initFPEWidget(ePost, fpe);
10411046
}/*replyClicked()*/;
10421047
10431048
const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{
@@ -1048,20 +1053,22 @@
10481053
fetchPost(fpid)
10491054
.then(artifact=>{
10501055
const ondone = (fpe, response)=>{
10511056
/* onsubmit() and ondiscard() callback */
10521057
//console.debug("ondiscard/onsubmit", fpe, eToDisable);
1053
- if( fpe/*onsubmit*/ ){
1058
+ if( response/*onsubmit()*/ ){
10541059
if( fpid === response.uuid
10551060
&& !response.statusModified
10561061
&& 0===response.attachedCount ){
10571062
fpe.reportError("No changes made.");
10581063
}else{
10591064
restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
10601065
window.location = F.repoUrl('forumpost/'+response.uuid);
10611066
}
10621067
}else{
1068
+ /*ondiscard()*/
1069
+ eBtnEdit.classList.remove('draft');
10631070
restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
10641071
}
10651072
};
10661073
const eStatusSelect = ePost.querySelector(
10671074
':scope > fieldset.forum-status-selection select[name=status]'
@@ -1069,11 +1076,11 @@
10691076
10701077
const fpe = new F.ForumPostEditor({
10711078
hiddenFields: form.querySelectorAll('input[type=hidden]'),
10721079
ondiscard: ondone,
10731080
onsubmit: ondone,
1074
- draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
1081
+ draftKey: makeDraftKey('draft-forumedit', fEditHead || fpid),
10751082
edit: artifact,
10761083
status: eStatusSelect?.value,
10771084
inReplyTo: firt
10781085
});
10791086
initFPEWidget(ePost, fpe);
@@ -1092,26 +1099,48 @@
10921099
if( !eThePost?.dataset?.fpid ){
10931100
/* The server injects these. */
10941101
console.warn("Unexpected missing fpid", eThePost);
10951102
return;
10961103
}
1104
+
1105
+ const checkButtonForDraft = (draftKeyPrefix, eBtn)=>{
1106
+ if( 1 ){
1107
+ /* 2026-06-09: this is currently disabled because it's
1108
+ much of the solution but not all of it. Still to solve
1109
+ is how to tag/untag these elements as local drafts are
1110
+ added/removed during this page's lifetime.
1111
+ ForumPostEditor does not have access to these buttons
1112
+ so can't flag them. We may need to add events to
1113
+ F.storage and monitor those.
1114
+ */
1115
+ return;
1116
+ }
1117
+ const fpid = eThePost.dataset.fpid;
1118
+ const fEditHead = eThePost.dataset.fedithead;
1119
+ const draftKey = makeDraftKey(draftKeyPrefix, fEditHead || fpid);
1120
+ if( F.storage.contains(draftKey) ){
1121
+ eBtn.classList.add('draft');
1122
+ }
1123
+ };
10971124
/* Replace the Reply and Edit buttons with ones which will activate
10981125
a ForumPostEditor. */
10991126
const btnReply = form.querySelector('input[type=submit][name=reply]');
11001127
if( btnReply ){
11011128
const b = D.button("Reply", ()=>replyClicked(form, eThePost, b, eToDisable));
11021129
b.type = 'button'/*keep container form from submitting*/;
11031130
eToDisable.push(b);
1131
+ checkButtonForDraft('draft-reply',b);
11041132
btnReply.parentElement.insertBefore(b, btnReply);
11051133
btnReply.remove();
11061134
}
11071135
const btnEdit = form.querySelector('input[type=submit][name=edit]');
11081136
if( btnEdit ){
11091137
//console.debug("hacking Edit button", btnEdit);
11101138
const b = D.button("Edit", ()=>editClicked(form, eThePost, b, eToDisable));
11111139
b.type = 'button';
11121140
eToDisable.push(b);
1141
+ checkButtonForDraft('draft-forumedit',b);
11131142
btnEdit.parentElement.insertBefore(b, btnEdit);
11141143
btnEdit.remove();
11151144
}
11161145
/* Problem: we really need to disable the status-selection
11171146
button because it would redirect mid-edit. We wouldn't lose
@@ -1134,9 +1163,9 @@
11341163
e.classList.remove('initially-hidden');
11351164
});
11361165
11371166
if( Date.now() % 17 === 0 ){
11381167
/* Purge old drafts only every now and then. */
1139
- F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/);
1168
+ F.ForumPostEditor.purgeOldDrafts(/^draft-(reply|forumedit)-.*/)/*not purging forumnew*/;
11401169
}
11411170
})/*F.onPageLoad callback*/;
11421171
})(window.fossil);
11431172
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -43,11 +43,11 @@
43 submit.
44
45 opt.ondiscard[=function]: if set, a Discard button is added
46 which, when activated, clears the current draft and removes
47 this object's widget from the DOM. After doing so,
48 opt.ondiscard() is called and passed no arguments. Exceptions
49 thrown by ondiscard() are ignored but may be logged.
50
51 opt.onsubmit[=function]: if set, this function is called
52 immediately after the post has been successfully saved, and
53 passed this object and a JSON-format response object from the
@@ -401,11 +401,11 @@
401 ** before the drafts are cleared and any exceptions it throws are
402 ** ignored (but may be logged).
403 */
404 discard(){
405 if( this.#opt.ondiscard instanceof Function ){
406 try{this.#opt.ondiscard();}
407 catch(e){
408 console.error("ForumPostEditor.ondiscard() threw:",e);
409 }
410 }
411 this.#clearDraft();
@@ -981,10 +981,14 @@
981 j = F.nu(j);
982 if( j.error ) throw new Error(j.error);
983 return j;
984 });
985 };
 
 
 
 
986
987 const setupEditReplyElement = (ePost, eButton, eToDisable)=>{
988 const fpid = ePost.dataset.fpid;
989 ePost.dataset.originalMarginLeft = ePost.style.marginLeft;
990 ePost.style.marginLeft = 'initial';
@@ -1011,13 +1015,15 @@
1011 eBtnReply.innerText = "Replying...";
1012 const ondone = (fpe, response)=>{
1013 /* onsubmit() and ondiscard() callback */
1014 restoreEditReplyElement(ePost, eBtnReply, eToDisable);
1015 //console.debug("ondiscard/onsubmit", fpe, artifact);
1016 if( response/*onsubmit*/ ){
1017 window.location = F.repoUrl('forumpost/'+response.uuid);
1018 fpe.close();
 
 
1019 }
1020 };
1021 const fpe = new F.ForumPostEditor({
1022 hiddenFields: form.querySelectorAll(
1023 'input[type=hidden][name=csrf]'
@@ -1025,19 +1031,18 @@
1025 an edit to that post rather than a response. */
1026 ),
1027 ondiscard: ondone,
1028 onsubmit: ondone,
1029 inReplyTo: fpid,
1030 draftKey: 'draft-reply-'+(
1031 fEditHead
1032 /* The problem with firt as a key is that firt is not
1033 necessarily the root edit of that post, which is
1034 what we really want as a draft key so that the
1035 draft does not disapper if firt is later edited
1036 (giving us a new firt value here). */
1037 || fpid
1038 ).substr(0,12)
1039 });
1040 initFPEWidget(ePost, fpe);
1041 }/*replyClicked()*/;
1042
1043 const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{
@@ -1048,20 +1053,22 @@
1048 fetchPost(fpid)
1049 .then(artifact=>{
1050 const ondone = (fpe, response)=>{
1051 /* onsubmit() and ondiscard() callback */
1052 //console.debug("ondiscard/onsubmit", fpe, eToDisable);
1053 if( fpe/*onsubmit*/ ){
1054 if( fpid === response.uuid
1055 && !response.statusModified
1056 && 0===response.attachedCount ){
1057 fpe.reportError("No changes made.");
1058 }else{
1059 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1060 window.location = F.repoUrl('forumpost/'+response.uuid);
1061 }
1062 }else{
 
 
1063 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1064 }
1065 };
1066 const eStatusSelect = ePost.querySelector(
1067 ':scope > fieldset.forum-status-selection select[name=status]'
@@ -1069,11 +1076,11 @@
1069
1070 const fpe = new F.ForumPostEditor({
1071 hiddenFields: form.querySelectorAll('input[type=hidden]'),
1072 ondiscard: ondone,
1073 onsubmit: ondone,
1074 draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
1075 edit: artifact,
1076 status: eStatusSelect?.value,
1077 inReplyTo: firt
1078 });
1079 initFPEWidget(ePost, fpe);
@@ -1092,26 +1099,48 @@
1092 if( !eThePost?.dataset?.fpid ){
1093 /* The server injects these. */
1094 console.warn("Unexpected missing fpid", eThePost);
1095 return;
1096 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1097 /* Replace the Reply and Edit buttons with ones which will activate
1098 a ForumPostEditor. */
1099 const btnReply = form.querySelector('input[type=submit][name=reply]');
1100 if( btnReply ){
1101 const b = D.button("Reply", ()=>replyClicked(form, eThePost, b, eToDisable));
1102 b.type = 'button'/*keep container form from submitting*/;
1103 eToDisable.push(b);
 
1104 btnReply.parentElement.insertBefore(b, btnReply);
1105 btnReply.remove();
1106 }
1107 const btnEdit = form.querySelector('input[type=submit][name=edit]');
1108 if( btnEdit ){
1109 //console.debug("hacking Edit button", btnEdit);
1110 const b = D.button("Edit", ()=>editClicked(form, eThePost, b, eToDisable));
1111 b.type = 'button';
1112 eToDisable.push(b);
 
1113 btnEdit.parentElement.insertBefore(b, btnEdit);
1114 btnEdit.remove();
1115 }
1116 /* Problem: we really need to disable the status-selection
1117 button because it would redirect mid-edit. We wouldn't lose
@@ -1134,9 +1163,9 @@
1134 e.classList.remove('initially-hidden');
1135 });
1136
1137 if( Date.now() % 17 === 0 ){
1138 /* Purge old drafts only every now and then. */
1139 F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/);
1140 }
1141 })/*F.onPageLoad callback*/;
1142 })(window.fossil);
1143
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -43,11 +43,11 @@
43 submit.
44
45 opt.ondiscard[=function]: if set, a Discard button is added
46 which, when activated, clears the current draft and removes
47 this object's widget from the DOM. After doing so,
48 opt.ondiscard() is called and passed this object. Exceptions
49 thrown by ondiscard() are ignored but may be logged.
50
51 opt.onsubmit[=function]: if set, this function is called
52 immediately after the post has been successfully saved, and
53 passed this object and a JSON-format response object from the
@@ -401,11 +401,11 @@
401 ** before the drafts are cleared and any exceptions it throws are
402 ** ignored (but may be logged).
403 */
404 discard(){
405 if( this.#opt.ondiscard instanceof Function ){
406 try{this.#opt.ondiscard(this);}
407 catch(e){
408 console.error("ForumPostEditor.ondiscard() threw:",e);
409 }
410 }
411 this.#clearDraft();
@@ -981,10 +981,14 @@
981 j = F.nu(j);
982 if( j.error ) throw new Error(j.error);
983 return j;
984 });
985 };
986
987 const makeDraftKey = (prefix,uuid)=>{
988 return prefix+'-'+uuid.substr(0,12);
989 };
990
991 const setupEditReplyElement = (ePost, eButton, eToDisable)=>{
992 const fpid = ePost.dataset.fpid;
993 ePost.dataset.originalMarginLeft = ePost.style.marginLeft;
994 ePost.style.marginLeft = 'initial';
@@ -1011,13 +1015,15 @@
1015 eBtnReply.innerText = "Replying...";
1016 const ondone = (fpe, response)=>{
1017 /* onsubmit() and ondiscard() callback */
1018 restoreEditReplyElement(ePost, eBtnReply, eToDisable);
1019 //console.debug("ondiscard/onsubmit", fpe, artifact);
1020 if( response/*onsubmit()*/ ){
1021 window.location = F.repoUrl('forumpost/'+response.uuid);
1022 fpe.close();
1023 }else{/*ondiscard()*/
1024 eBtnReply.classList.remove('draft');
1025 }
1026 };
1027 const fpe = new F.ForumPostEditor({
1028 hiddenFields: form.querySelectorAll(
1029 'input[type=hidden][name=csrf]'
@@ -1025,19 +1031,18 @@
1031 an edit to that post rather than a response. */
1032 ),
1033 ondiscard: ondone,
1034 onsubmit: ondone,
1035 inReplyTo: fpid,
1036 draftKey: makeDraftKey('draft-reply', fEditHead
 
1037 /* The problem with firt as a key is that firt is not
1038 necessarily the root edit of that post, which is
1039 what we really want as a draft key so that the
1040 draft does not disapper if firt is later edited
1041 (giving us a new firt value here). */
1042 || fpid
1043 )
1044 });
1045 initFPEWidget(ePost, fpe);
1046 }/*replyClicked()*/;
1047
1048 const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{
@@ -1048,20 +1053,22 @@
1053 fetchPost(fpid)
1054 .then(artifact=>{
1055 const ondone = (fpe, response)=>{
1056 /* onsubmit() and ondiscard() callback */
1057 //console.debug("ondiscard/onsubmit", fpe, eToDisable);
1058 if( response/*onsubmit()*/ ){
1059 if( fpid === response.uuid
1060 && !response.statusModified
1061 && 0===response.attachedCount ){
1062 fpe.reportError("No changes made.");
1063 }else{
1064 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1065 window.location = F.repoUrl('forumpost/'+response.uuid);
1066 }
1067 }else{
1068 /*ondiscard()*/
1069 eBtnEdit.classList.remove('draft');
1070 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1071 }
1072 };
1073 const eStatusSelect = ePost.querySelector(
1074 ':scope > fieldset.forum-status-selection select[name=status]'
@@ -1069,11 +1076,11 @@
1076
1077 const fpe = new F.ForumPostEditor({
1078 hiddenFields: form.querySelectorAll('input[type=hidden]'),
1079 ondiscard: ondone,
1080 onsubmit: ondone,
1081 draftKey: makeDraftKey('draft-forumedit', fEditHead || fpid),
1082 edit: artifact,
1083 status: eStatusSelect?.value,
1084 inReplyTo: firt
1085 });
1086 initFPEWidget(ePost, fpe);
@@ -1092,26 +1099,48 @@
1099 if( !eThePost?.dataset?.fpid ){
1100 /* The server injects these. */
1101 console.warn("Unexpected missing fpid", eThePost);
1102 return;
1103 }
1104
1105 const checkButtonForDraft = (draftKeyPrefix, eBtn)=>{
1106 if( 1 ){
1107 /* 2026-06-09: this is currently disabled because it's
1108 much of the solution but not all of it. Still to solve
1109 is how to tag/untag these elements as local drafts are
1110 added/removed during this page's lifetime.
1111 ForumPostEditor does not have access to these buttons
1112 so can't flag them. We may need to add events to
1113 F.storage and monitor those.
1114 */
1115 return;
1116 }
1117 const fpid = eThePost.dataset.fpid;
1118 const fEditHead = eThePost.dataset.fedithead;
1119 const draftKey = makeDraftKey(draftKeyPrefix, fEditHead || fpid);
1120 if( F.storage.contains(draftKey) ){
1121 eBtn.classList.add('draft');
1122 }
1123 };
1124 /* Replace the Reply and Edit buttons with ones which will activate
1125 a ForumPostEditor. */
1126 const btnReply = form.querySelector('input[type=submit][name=reply]');
1127 if( btnReply ){
1128 const b = D.button("Reply", ()=>replyClicked(form, eThePost, b, eToDisable));
1129 b.type = 'button'/*keep container form from submitting*/;
1130 eToDisable.push(b);
1131 checkButtonForDraft('draft-reply',b);
1132 btnReply.parentElement.insertBefore(b, btnReply);
1133 btnReply.remove();
1134 }
1135 const btnEdit = form.querySelector('input[type=submit][name=edit]');
1136 if( btnEdit ){
1137 //console.debug("hacking Edit button", btnEdit);
1138 const b = D.button("Edit", ()=>editClicked(form, eThePost, b, eToDisable));
1139 b.type = 'button';
1140 eToDisable.push(b);
1141 checkButtonForDraft('draft-forumedit',b);
1142 btnEdit.parentElement.insertBefore(b, btnEdit);
1143 btnEdit.remove();
1144 }
1145 /* Problem: we really need to disable the status-selection
1146 button because it would redirect mid-edit. We wouldn't lose
@@ -1134,9 +1163,9 @@
1163 e.classList.remove('initially-hidden');
1164 });
1165
1166 if( Date.now() % 17 === 0 ){
1167 /* Purge old drafts only every now and then. */
1168 F.ForumPostEditor.purgeOldDrafts(/^draft-(reply|forumedit)-.*/)/*not purging forumnew*/;
1169 }
1170 })/*F.onPageLoad callback*/;
1171 })(window.fossil);
1172
--- src/style.forum.css
+++ src/style.forum.css
@@ -4,10 +4,14 @@
44
max-width: max-content;
55
border-radius: 0.5em;
66
padding: 0 0.5em;
77
margin-bottom: 0.35em;
88
}
9
+
10
+body.forum .forumpost-single-controls button.draft:after {
11
+ content: " [draft]";
12
+}
913
1014
.ForumPostEditor {
1115
display: flex;
1216
flex-direction: column;
1317
gap: 1em;
1418
--- src/style.forum.css
+++ src/style.forum.css
@@ -4,10 +4,14 @@
4 max-width: max-content;
5 border-radius: 0.5em;
6 padding: 0 0.5em;
7 margin-bottom: 0.35em;
8 }
 
 
 
 
9
10 .ForumPostEditor {
11 display: flex;
12 flex-direction: column;
13 gap: 1em;
14
--- src/style.forum.css
+++ src/style.forum.css
@@ -4,10 +4,14 @@
4 max-width: max-content;
5 border-radius: 0.5em;
6 padding: 0 0.5em;
7 margin-bottom: 0.35em;
8 }
9
10 body.forum .forumpost-single-controls button.draft:after {
11 content: " [draft]";
12 }
13
14 .ForumPostEditor {
15 display: flex;
16 flex-direction: column;
17 gap: 1em;
18

Keyboard Shortcuts

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