Fossil SCM

When submitting edits to a post, do not save a new copy if nothing modified and instead alert the user. Remove the Status tag widget from the editor because an apparent bug is causing that tag to be applied on each save, even if it's not modified, and that needs to be chased down.

stephan 2026-06-09 18:32 UTC forum-editor-2026
Commit 598d45f816991ec1fd7742ecd02d16eca2e677bd2e0931454ea6b1336349853b
+77 -18
--- src/forum.c
+++ src/forum.c
@@ -902,11 +902,12 @@
902902
assert( sCurrent );
903903
@ <fieldset class='forum-status-selection'>\
904904
@ <legend>Status \
905905
@ <span class='help-buttonlet initially-hidden'>\
906906
@ Moderators and the post's owner may change \
907
- @ the status of this thread. See \
907
+ @ the status of this thread unless it is still. \
908
+ @ pending moderation. See \
908909
@ <a href='%R/help/forum-statuses' target='_new'>\
909910
@ /help/forum-statuses</a></span>\
910911
@ </legend>\
911912
if( forum_may_set_status(fp->fpid) ){
912913
@ <form method="post" action='%R/forumpost_status'>
@@ -2820,10 +2821,16 @@
28202821
**
28212822
** The caller must have started a transaction and must roll it back if
28222823
** this call returns <=0, noting that only the negative-value case is
28232824
** an error.
28242825
**
2826
+** This function does some work to try to ensure that duplicate
2827
+** entries are not save (this can happen as a side effect of the forum
2828
+** post editor added in 2026-06). If the given post will not have been
2829
+** materially edited by these changes, they are not applied and the
2830
+** rid of the existing entry is used.
2831
+**
28252832
** Maintenance reminders:
28262833
**
28272834
** - iInReplyTo==0 && iEdit==0: new thread
28282835
** - iInReplyTo==0 && iEdit>0 : edit top post or response
28292836
** - iInReplyTo>0 && iEdit==0: new response
@@ -2838,10 +2845,11 @@
28382845
const char *zContent, /* Content */
28392846
int iFlags /* FPOST_xyz flag values */
28402847
){
28412848
char *zI;
28422849
char *zG;
2850
+ char *zP = 0;
28432851
int iBasis;
28442852
Blob x = BLOB_INITIALIZER,
28452853
cksum = BLOB_INITIALIZER,
28462854
formatCheck = BLOB_INITIALIZER,
28472855
errMsg = BLOB_INITIALIZER;
@@ -2885,12 +2893,64 @@
28852893
/* TODO (2026-06-008) If (iInReplyTo>0 && iEdit>0), validate that
28862894
** iInReplyTo is connected to iEdit properly, else we risk
28872895
** reparenting the new edit and having unrepredictable downstream
28882896
** side effects. */
28892897
}
2898
+
2899
+ if( 0!=zMimetype && 0==zMimetype[0] ){
2900
+ zMimetype = 0;
2901
+ }
2902
+
28902903
if( 0!=zTitle && 0==zTitle[0] ) zTitle = 0;
28912904
webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
2905
+
2906
+ if( iEdit>0 ){
2907
+ int cmp;
2908
+ pPost = manifest_get(iEdit, CFTYPE_FORUM, 0);
2909
+ if( pPost==0 ){
2910
+ rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit);
2911
+ goto post_ajax_end;
2912
+ }
2913
+ /*
2914
+ ** If the old content matches the new then do not save a new copy.
2915
+ ** It's easy to get re-posts of unedited content via the forum
2916
+ ** editor, especially since the one added in 2026-06, where a
2917
+ ** post's status and attachments may be amended from the editor
2918
+ ** without modifying any of the post's content. In the legacy
2919
+ ** editor such "out-of-band" changes weren't possible and users
2920
+ ** have never made a practice of re-posting unedited content.
2921
+ **
2922
+ ** We compare the following fields to the original: user, mimetype,
2923
+ ** content, and (for root posts only) the title.
2924
+ */
2925
+ cmp = (0==pPost->zInReplyTo)
2926
+ ? fossil_strcmp(pPost->zThreadTitle, zTitle)
2927
+ : 0;
2928
+ if( 0==cmp ){
2929
+ cmp=fossil_strcmp(pPost->zWiki, zContent);
2930
+ if( 0==cmp ){
2931
+ cmp = fossil_strcmp(pPost->zUser, zUser);
2932
+ }
2933
+ if( 0==cmp
2934
+ && 0!=(cmp=fossil_strcmp(pPost->zMimetype, zMimetype)) ){
2935
+ /* Extra mimetype checks for a common condition seen elsewhere */
2936
+ if( (0==zMimetype
2937
+ && 0==fossil_strcmp(pPost->zMimetype, "text/x-fossil-wiki"))
2938
+ || (0==pPost->zMimetype
2939
+ && 0==fossil_strcmp(zMimetype, "text/x-fossil-wiki")) ){
2940
+ cmp = 0;
2941
+ }
2942
+ }
2943
+ if( 0==cmp ){
2944
+ rc = iEdit;
2945
+ goto post_ajax_end;
2946
+ }
2947
+ }
2948
+ zP = rid_to_uuid(iEdit);
2949
+ }
2950
+
2951
+ /* Write the new artifact */
28922952
blob_init(&x, 0, 0);
28932953
blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
28942954
zG = db_text(
28952955
0,
28962956
"SELECT uuid FROM blob, forumpost"
@@ -2912,21 +2972,15 @@
29122972
goto post_ajax_end;
29132973
}
29142974
blob_appendf(&x, "I %z\n", zI);
29152975
}
29162976
if( zMimetype!=0
2917
- && zMimetype[0]!=0
29182977
&& fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
29192978
blob_appendf(&x, "N %F\n", zMimetype);
29202979
}
2921
- if( iEdit>0 ){
2922
- char *zP = rid_to_uuid(iEdit);
2923
- if( zP==0 ){
2924
- rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit);
2925
- goto post_ajax_end;
2926
- }
2927
- blob_appendf(&x, "P %z\n", zP);
2980
+ if( zP ){
2981
+ blob_appendf(&x, "P %s\n", zP);
29282982
}
29292983
29302984
blob_appendf(&x, "U %F\n", zUser);
29312985
blob_appendf(&x, "W %d\n%s\n", nContent, zContent);
29322986
md5sum_blob(&x, &cksum);
@@ -2958,10 +3012,11 @@
29583012
rc = nrid;
29593013
db_end_transaction(0);
29603014
}
29613015
post_ajax_end:
29623016
manifest_destroy(pPost);
3017
+ fossil_free(zP);
29633018
blob_reset(&x);
29643019
blob_reset(&cksum);
29653020
blob_reset(&formatCheck);
29663021
return rc;
29673022
}
@@ -2988,10 +3043,12 @@
29883043
int fpid = 0; /* Post rid being edited or 0 */
29893044
int rc = 0; /* Result code. */
29903045
int nrid = 0; /* New artifact rid. */
29913046
int iPostFlags; /* forum_post_flags() (after perms check) */
29923047
int bRollback; /* True = roll back. */
3048
+ int nAttach = 0; /* Number of attachments added */
3049
+ int bStatusSet = 0; /* True if status tag set. */
29933050
29943051
if( !ajax_route_bootstrap(0, 1) ){
29953052
return;
29963053
}else if( !g.perm.WrForum
29973054
|| (bHasAttachment && !g.perm.AttachForum) ){
@@ -3078,19 +3135,18 @@
30783135
if( !g.perm.Admin && !g.perm.AttachForum ){
30793136
rc = -ajax_route_error(403, "No permission no attach files.");
30803137
goto ajax_save_end;
30813138
}else{
30823139
char *zRoot = (nrid==fpHead) ? 0 : rid_to_uuid(fpHead);
3083
- const int atRc =
3084
- attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3085
- bNeedsModeration);
3140
+ nAttach = attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3141
+ bNeedsModeration);
30863142
fossil_free(zRoot);
3087
- if( atRc<0 ){
3088
- rc = atRc;
3143
+ if( nAttach<0 ){
3144
+ rc = nAttach;
30893145
goto ajax_save_end;
30903146
}
3091
- if( atRc>0
3147
+ if( nAttach>0
30923148
&& (iPostFlags & FPOST_NO_ALERT)!=0
30933149
&& db_table_exists("repository","pending_alert") ){
30943150
/* Unqueue any alerts for these attachments. Recall that
30953151
** they're attached to the first version of the post, which
30963152
** means we actually risk cancelling _other_ pending
@@ -3113,21 +3169,24 @@
31133169
/* ^^^ Do not allow a status tag on a pending-moderation post
31143170
** because it will introduce a reference to an artifact which
31153171
** will become a phantom if it is rejected by a moderator. */
31163172
&& zStatus!=0 && zStatus[0]!=0
31173173
&& forum_may_set_status(nrid)
3118
- && forumpost_tag(nrid, 1, "status", zStatus)<0 ){
3174
+ && (bStatusSet=forumpost_tag(nrid, 1, "status", zStatus))<0 ){
31193175
rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg);
31203176
goto ajax_save_end;
31213177
}
31223178
}
31233179
31243180
assert( 0==rc );
31253181
assert( zNewUuid );
3126
- CX("{\"uuid\": %!j, \"dryrun\": %s, \"iPostFlags\":%d}\n",
3127
- zNewUuid, bRollback ? "true" : "false", iPostFlags);
3182
+ CX("{\"uuid\": %!j, \"attachedCount\": %d, "
3183
+ "\"statusModified\": %d, "
3184
+ "\"dryrun\": %s, \"iPostFlags\":%d}\n",
3185
+ zNewUuid, nAttach,
3186
+ bStatusSet, bRollback ? "true" : "false", iPostFlags);
31283187
31293188
ajax_save_end:
31303189
manifest_destroy(pPost);
31313190
fossil_free(zNewUuid);
31323191
db_end_transaction(rc || bRollback);
31333192
}
31343193
--- src/forum.c
+++ src/forum.c
@@ -902,11 +902,12 @@
902 assert( sCurrent );
903 @ <fieldset class='forum-status-selection'>\
904 @ <legend>Status \
905 @ <span class='help-buttonlet initially-hidden'>\
906 @ Moderators and the post's owner may change \
907 @ the status of this thread. See \
 
908 @ <a href='%R/help/forum-statuses' target='_new'>\
909 @ /help/forum-statuses</a></span>\
910 @ </legend>\
911 if( forum_may_set_status(fp->fpid) ){
912 @ <form method="post" action='%R/forumpost_status'>
@@ -2820,10 +2821,16 @@
2820 **
2821 ** The caller must have started a transaction and must roll it back if
2822 ** this call returns <=0, noting that only the negative-value case is
2823 ** an error.
2824 **
 
 
 
 
 
 
2825 ** Maintenance reminders:
2826 **
2827 ** - iInReplyTo==0 && iEdit==0: new thread
2828 ** - iInReplyTo==0 && iEdit>0 : edit top post or response
2829 ** - iInReplyTo>0 && iEdit==0: new response
@@ -2838,10 +2845,11 @@
2838 const char *zContent, /* Content */
2839 int iFlags /* FPOST_xyz flag values */
2840 ){
2841 char *zI;
2842 char *zG;
 
2843 int iBasis;
2844 Blob x = BLOB_INITIALIZER,
2845 cksum = BLOB_INITIALIZER,
2846 formatCheck = BLOB_INITIALIZER,
2847 errMsg = BLOB_INITIALIZER;
@@ -2885,12 +2893,64 @@
2885 /* TODO (2026-06-008) If (iInReplyTo>0 && iEdit>0), validate that
2886 ** iInReplyTo is connected to iEdit properly, else we risk
2887 ** reparenting the new edit and having unrepredictable downstream
2888 ** side effects. */
2889 }
 
 
 
 
 
2890 if( 0!=zTitle && 0==zTitle[0] ) zTitle = 0;
2891 webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2892 blob_init(&x, 0, 0);
2893 blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
2894 zG = db_text(
2895 0,
2896 "SELECT uuid FROM blob, forumpost"
@@ -2912,21 +2972,15 @@
2912 goto post_ajax_end;
2913 }
2914 blob_appendf(&x, "I %z\n", zI);
2915 }
2916 if( zMimetype!=0
2917 && zMimetype[0]!=0
2918 && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
2919 blob_appendf(&x, "N %F\n", zMimetype);
2920 }
2921 if( iEdit>0 ){
2922 char *zP = rid_to_uuid(iEdit);
2923 if( zP==0 ){
2924 rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit);
2925 goto post_ajax_end;
2926 }
2927 blob_appendf(&x, "P %z\n", zP);
2928 }
2929
2930 blob_appendf(&x, "U %F\n", zUser);
2931 blob_appendf(&x, "W %d\n%s\n", nContent, zContent);
2932 md5sum_blob(&x, &cksum);
@@ -2958,10 +3012,11 @@
2958 rc = nrid;
2959 db_end_transaction(0);
2960 }
2961 post_ajax_end:
2962 manifest_destroy(pPost);
 
2963 blob_reset(&x);
2964 blob_reset(&cksum);
2965 blob_reset(&formatCheck);
2966 return rc;
2967 }
@@ -2988,10 +3043,12 @@
2988 int fpid = 0; /* Post rid being edited or 0 */
2989 int rc = 0; /* Result code. */
2990 int nrid = 0; /* New artifact rid. */
2991 int iPostFlags; /* forum_post_flags() (after perms check) */
2992 int bRollback; /* True = roll back. */
 
 
2993
2994 if( !ajax_route_bootstrap(0, 1) ){
2995 return;
2996 }else if( !g.perm.WrForum
2997 || (bHasAttachment && !g.perm.AttachForum) ){
@@ -3078,19 +3135,18 @@
3078 if( !g.perm.Admin && !g.perm.AttachForum ){
3079 rc = -ajax_route_error(403, "No permission no attach files.");
3080 goto ajax_save_end;
3081 }else{
3082 char *zRoot = (nrid==fpHead) ? 0 : rid_to_uuid(fpHead);
3083 const int atRc =
3084 attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3085 bNeedsModeration);
3086 fossil_free(zRoot);
3087 if( atRc<0 ){
3088 rc = atRc;
3089 goto ajax_save_end;
3090 }
3091 if( atRc>0
3092 && (iPostFlags & FPOST_NO_ALERT)!=0
3093 && db_table_exists("repository","pending_alert") ){
3094 /* Unqueue any alerts for these attachments. Recall that
3095 ** they're attached to the first version of the post, which
3096 ** means we actually risk cancelling _other_ pending
@@ -3113,21 +3169,24 @@
3113 /* ^^^ Do not allow a status tag on a pending-moderation post
3114 ** because it will introduce a reference to an artifact which
3115 ** will become a phantom if it is rejected by a moderator. */
3116 && zStatus!=0 && zStatus[0]!=0
3117 && forum_may_set_status(nrid)
3118 && forumpost_tag(nrid, 1, "status", zStatus)<0 ){
3119 rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg);
3120 goto ajax_save_end;
3121 }
3122 }
3123
3124 assert( 0==rc );
3125 assert( zNewUuid );
3126 CX("{\"uuid\": %!j, \"dryrun\": %s, \"iPostFlags\":%d}\n",
3127 zNewUuid, bRollback ? "true" : "false", iPostFlags);
 
 
 
3128
3129 ajax_save_end:
3130 manifest_destroy(pPost);
3131 fossil_free(zNewUuid);
3132 db_end_transaction(rc || bRollback);
3133 }
3134
--- src/forum.c
+++ src/forum.c
@@ -902,11 +902,12 @@
902 assert( sCurrent );
903 @ <fieldset class='forum-status-selection'>\
904 @ <legend>Status \
905 @ <span class='help-buttonlet initially-hidden'>\
906 @ Moderators and the post's owner may change \
907 @ the status of this thread unless it is still. \
908 @ pending moderation. See \
909 @ <a href='%R/help/forum-statuses' target='_new'>\
910 @ /help/forum-statuses</a></span>\
911 @ </legend>\
912 if( forum_may_set_status(fp->fpid) ){
913 @ <form method="post" action='%R/forumpost_status'>
@@ -2820,10 +2821,16 @@
2821 **
2822 ** The caller must have started a transaction and must roll it back if
2823 ** this call returns <=0, noting that only the negative-value case is
2824 ** an error.
2825 **
2826 ** This function does some work to try to ensure that duplicate
2827 ** entries are not save (this can happen as a side effect of the forum
2828 ** post editor added in 2026-06). If the given post will not have been
2829 ** materially edited by these changes, they are not applied and the
2830 ** rid of the existing entry is used.
2831 **
2832 ** Maintenance reminders:
2833 **
2834 ** - iInReplyTo==0 && iEdit==0: new thread
2835 ** - iInReplyTo==0 && iEdit>0 : edit top post or response
2836 ** - iInReplyTo>0 && iEdit==0: new response
@@ -2838,10 +2845,11 @@
2845 const char *zContent, /* Content */
2846 int iFlags /* FPOST_xyz flag values */
2847 ){
2848 char *zI;
2849 char *zG;
2850 char *zP = 0;
2851 int iBasis;
2852 Blob x = BLOB_INITIALIZER,
2853 cksum = BLOB_INITIALIZER,
2854 formatCheck = BLOB_INITIALIZER,
2855 errMsg = BLOB_INITIALIZER;
@@ -2885,12 +2893,64 @@
2893 /* TODO (2026-06-008) If (iInReplyTo>0 && iEdit>0), validate that
2894 ** iInReplyTo is connected to iEdit properly, else we risk
2895 ** reparenting the new edit and having unrepredictable downstream
2896 ** side effects. */
2897 }
2898
2899 if( 0!=zMimetype && 0==zMimetype[0] ){
2900 zMimetype = 0;
2901 }
2902
2903 if( 0!=zTitle && 0==zTitle[0] ) zTitle = 0;
2904 webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
2905
2906 if( iEdit>0 ){
2907 int cmp;
2908 pPost = manifest_get(iEdit, CFTYPE_FORUM, 0);
2909 if( pPost==0 ){
2910 rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit);
2911 goto post_ajax_end;
2912 }
2913 /*
2914 ** If the old content matches the new then do not save a new copy.
2915 ** It's easy to get re-posts of unedited content via the forum
2916 ** editor, especially since the one added in 2026-06, where a
2917 ** post's status and attachments may be amended from the editor
2918 ** without modifying any of the post's content. In the legacy
2919 ** editor such "out-of-band" changes weren't possible and users
2920 ** have never made a practice of re-posting unedited content.
2921 **
2922 ** We compare the following fields to the original: user, mimetype,
2923 ** content, and (for root posts only) the title.
2924 */
2925 cmp = (0==pPost->zInReplyTo)
2926 ? fossil_strcmp(pPost->zThreadTitle, zTitle)
2927 : 0;
2928 if( 0==cmp ){
2929 cmp=fossil_strcmp(pPost->zWiki, zContent);
2930 if( 0==cmp ){
2931 cmp = fossil_strcmp(pPost->zUser, zUser);
2932 }
2933 if( 0==cmp
2934 && 0!=(cmp=fossil_strcmp(pPost->zMimetype, zMimetype)) ){
2935 /* Extra mimetype checks for a common condition seen elsewhere */
2936 if( (0==zMimetype
2937 && 0==fossil_strcmp(pPost->zMimetype, "text/x-fossil-wiki"))
2938 || (0==pPost->zMimetype
2939 && 0==fossil_strcmp(zMimetype, "text/x-fossil-wiki")) ){
2940 cmp = 0;
2941 }
2942 }
2943 if( 0==cmp ){
2944 rc = iEdit;
2945 goto post_ajax_end;
2946 }
2947 }
2948 zP = rid_to_uuid(iEdit);
2949 }
2950
2951 /* Write the new artifact */
2952 blob_init(&x, 0, 0);
2953 blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
2954 zG = db_text(
2955 0,
2956 "SELECT uuid FROM blob, forumpost"
@@ -2912,21 +2972,15 @@
2972 goto post_ajax_end;
2973 }
2974 blob_appendf(&x, "I %z\n", zI);
2975 }
2976 if( zMimetype!=0
 
2977 && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
2978 blob_appendf(&x, "N %F\n", zMimetype);
2979 }
2980 if( zP ){
2981 blob_appendf(&x, "P %s\n", zP);
 
 
 
 
 
2982 }
2983
2984 blob_appendf(&x, "U %F\n", zUser);
2985 blob_appendf(&x, "W %d\n%s\n", nContent, zContent);
2986 md5sum_blob(&x, &cksum);
@@ -2958,10 +3012,11 @@
3012 rc = nrid;
3013 db_end_transaction(0);
3014 }
3015 post_ajax_end:
3016 manifest_destroy(pPost);
3017 fossil_free(zP);
3018 blob_reset(&x);
3019 blob_reset(&cksum);
3020 blob_reset(&formatCheck);
3021 return rc;
3022 }
@@ -2988,10 +3043,12 @@
3043 int fpid = 0; /* Post rid being edited or 0 */
3044 int rc = 0; /* Result code. */
3045 int nrid = 0; /* New artifact rid. */
3046 int iPostFlags; /* forum_post_flags() (after perms check) */
3047 int bRollback; /* True = roll back. */
3048 int nAttach = 0; /* Number of attachments added */
3049 int bStatusSet = 0; /* True if status tag set. */
3050
3051 if( !ajax_route_bootstrap(0, 1) ){
3052 return;
3053 }else if( !g.perm.WrForum
3054 || (bHasAttachment && !g.perm.AttachForum) ){
@@ -3078,19 +3135,18 @@
3135 if( !g.perm.Admin && !g.perm.AttachForum ){
3136 rc = -ajax_route_error(403, "No permission no attach files.");
3137 goto ajax_save_end;
3138 }else{
3139 char *zRoot = (nrid==fpHead) ? 0 : rid_to_uuid(fpHead);
3140 nAttach = attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3141 bNeedsModeration);
 
3142 fossil_free(zRoot);
3143 if( nAttach<0 ){
3144 rc = nAttach;
3145 goto ajax_save_end;
3146 }
3147 if( nAttach>0
3148 && (iPostFlags & FPOST_NO_ALERT)!=0
3149 && db_table_exists("repository","pending_alert") ){
3150 /* Unqueue any alerts for these attachments. Recall that
3151 ** they're attached to the first version of the post, which
3152 ** means we actually risk cancelling _other_ pending
@@ -3113,21 +3169,24 @@
3169 /* ^^^ Do not allow a status tag on a pending-moderation post
3170 ** because it will introduce a reference to an artifact which
3171 ** will become a phantom if it is rejected by a moderator. */
3172 && zStatus!=0 && zStatus[0]!=0
3173 && forum_may_set_status(nrid)
3174 && (bStatusSet=forumpost_tag(nrid, 1, "status", zStatus))<0 ){
3175 rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg);
3176 goto ajax_save_end;
3177 }
3178 }
3179
3180 assert( 0==rc );
3181 assert( zNewUuid );
3182 CX("{\"uuid\": %!j, \"attachedCount\": %d, "
3183 "\"statusModified\": %d, "
3184 "\"dryrun\": %s, \"iPostFlags\":%d}\n",
3185 zNewUuid, nAttach,
3186 bStatusSet, bRollback ? "true" : "false", iPostFlags);
3187
3188 ajax_save_end:
3189 manifest_destroy(pPost);
3190 fossil_free(zNewUuid);
3191 db_end_transaction(rc || bRollback);
3192 }
3193
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -41,16 +41,21 @@
4141
lose focus. If old state is found, the form is pre-populated
4242
from it. The state is cleared on a successful submit.
4343
4444
opt.ondiscard[=function]: if set, a Discard button is added
4545
which, when activated, clears the current draft and removes
46
- this object's widget from the DOM. After that, opt.ondiscard()
47
- is called and passed no arguments.
46
+ this object's widget from the DOM. After doing so,
47
+ opt.ondiscard() is called and passed no arguments. Exceptions
48
+ thrown by ondiscard() are ignored but may be logged.
4849
4950
opt.onsubmit[=function]: if set, this function is called
50
- immediately after the post has been successfully saved,
51
- and passed this object.
51
+ immediately after the post has been successfully saved, and
52
+ passed this object and a JSON-format response object from the
53
+ save request. On a successful edit, if the artifact was
54
+ actually modified then submission will redirect the page to
55
+ /forumpost/THE_POST_ID. Exceptions thrown by onsubmit()
56
+ are ignored but may be logged.
5257
5358
opt.hiddenFields: an optional list of input elements to
5459
incorporate into the form for requests which request the
5560
preview or save the post.
5661
@@ -152,10 +157,13 @@
152157
onconfirm: ()=>this.discard()
153158
});
154159
}
155160
}else{
156161
e.button.submit.addEventListener('click', ()=>this.#submit());
162
+ if( e.button.discard ){
163
+ e.button.submit.addEventListener('click', ()=>this.discard());
164
+ }
157165
}
158166
e.button.submit.setAttribute('disabled', '');
159167
wrapper.append(e.buttons);
160168
161169
e.error = D.addClass(D.div(), 'error', 'hidden');
@@ -249,11 +257,17 @@
249257
}
250258
this.#tabs.addTab(e.debug);
251259
}
252260
e.buttons.append(e.mimetype.wrapper);
253261
254
- if( opt.edit
262
+ if( 0 /* 2026-06-09: disabling the status selection to keep
263
+ people from using the editor to change just that,
264
+ because doing so leaves us in a staet where we know
265
+ whether or not a Submit modifies the post, but not
266
+ whether or not out-of-band state like the status and
267
+ attachments were modified. */
268
+ && opt.edit
255269
&& !opt.inReplyTo
256270
&& F.config.forumStatuses?.length>0 ){
257271
/* Status selection. We probably don't _really_ want this in
258272
the editor because people will open the editor, change the
259273
status, and tap submit, resulting in a whole new, unedited
@@ -372,10 +386,14 @@
372386
e.buttons.append(e.button.toggleHeader);
373387
}
374388
375389
}/*constructor*/
376390
391
+ /*
392
+ ** Removes this object from the DOM. It has no side effects if
393
+ ** it's not in the DOM.
394
+ */
377395
close(){
378396
const e = this.#e.widget;
379397
if( e?.parentNode ){
380398
if( this.#opt.onclose instanceof Function ){
381399
try{this.#opt.onclose();}
@@ -387,10 +405,16 @@
387405
e.classList.add('animate-exit');
388406
e.addEventListener('animationend', ()=>e.remove(), {once: true});
389407
}
390408
}
391409
410
+ /*
411
+ ** Discards any draft edits then calls close(). If an ondiscard
412
+ ** callback was provided to the constructor then it is called
413
+ ** before the drafts are cleared and any exceptions it throws are
414
+ ** ignored (but may be logged).
415
+ */
392416
discard(){
393417
if( this.#opt.ondiscard instanceof Function ){
394418
try{this.#opt.ondiscard();}
395419
catch(e){
396420
console.error("ForumPostEditor.ondiscard() threw:",e);
@@ -426,11 +450,15 @@
426450
const e = this.#e.error;
427451
D.clearElement(e);
428452
if( msg.length ){
429453
console.error('ForumPostEditor:',...msg);
430454
e.classList.remove('hidden');
431
- e.append(...msg);
455
+ e.append(
456
+ ...msg, D.br(),
457
+ D.button("Clear", ()=>this.reportError())
458
+ /* Looks horrid in the Blitz skin */
459
+ );
432460
}else{
433461
e.classList.add('hidden');
434462
}
435463
}
436464
@@ -654,16 +682,32 @@
654682
return;
655683
}
656684
if( 1 ){
657685
this.#clearDraft();
658686
if( this.#opt.onsubmit instanceof Function ){
659
- try{this.#opt.onsubmit(this);}
687
+ try{this.#opt.onsubmit(this, j);}
660688
catch(e){
661689
console.error("ForumPostEditor.onsubmit() threw: ", e);
662690
}
663691
}
664
- window.location = F.repoUrl('forumpost/'+j.uuid);
692
+ /*
693
+ if( this.#opt.edit?.uuid === j.uuid ) then we know the
694
+ content did not change, but it's possible that attachments
695
+ and/or a status tag did. Ergo, we need to unconditionally
696
+ reload to render those changes (if any). The other option
697
+ is to tell the user "nothing changed" and leave them in
698
+ the editor, but that could be a lie because we don't know
699
+ if any attachments or tags were changed.
700
+ */
701
+ if( 0 ){
702
+ if( this.#opt.edit.uuid === j.uuid
703
+ && !j.statusModified && 0===j.attachedCount ){
704
+ this.reportError("No changes made.");
705
+ }else{
706
+ window.location = F.repoUrl('forumpost/'+j.uuid);
707
+ }
708
+ }
665709
}else{
666710
this.reportError(
667711
"Saving worked but we're ignoring it and staying here."
668712
);
669713
}
@@ -896,12 +940,10 @@
896940
});
897941
});
898942
});
899943
}
900944
901
- F.user.isIndividual = ['anonymous','nobody'].indexOf(F.user.name)<0;
902
-
903945
/* Page-specific style tweaks for a ForumPostEditor instance. */
904946
const initFPEWidget = (ePost, fpe)=>{
905947
const w = fpe.widget;
906948
w.style.borderTop = '1px dotted';
907949
//w.style.marginTop = '0.35em';
@@ -928,10 +970,13 @@
928970
const fpe = new F.ForumPostEditor({
929971
draftKey: 'draft-forumnew',
930972
hiddenFields: eForumNew.querySelectorAll('input[type=hidden]'),
931973
ondiscard: ()=>{
932974
window.location = F.repoUrl('forum');
975
+ },
976
+ onsubmit: (fpe, artifact)=>{
977
+ window.location = F.repoUrl('forumpost/'+artifact.uuid);
933978
}
934979
});
935980
eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
936981
eForumNew.remove();
937982
fossil.page.fpe = fpe /* for testing via the console */;
@@ -974,17 +1019,17 @@
9741019
9751020
const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{
9761021
const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable);
9771022
const fEditHead = ePost.dataset.fedithead;
9781023
eBtnReply.innerText = "Replying...";
979
- const ondone = (fpe)=>{
1024
+ const ondone = (fpe, response)=>{
1025
+ /* onsubmit() and ondiscard() callback */
9801026
restoreEditReplyElement(ePost, eBtnReply, eToDisable);
981
- //console.debug("ondiscard/onsubmit", fpe, eToDisable);
982
- if( fpe/*onsubmit*/ ){
983
- if( fpe.widget.parentNode ){
984
- fpe.widget.remove();
985
- }
1027
+ //console.debug("ondiscard/onsubmit", fpe, artifact);
1028
+ if( response/*onsubmit*/ ){
1029
+ window.location = F.repoUrl('forumpost/'+response.uuid);
1030
+ fpe.close();
9861031
}
9871032
};
9881033
const fpe = new F.ForumPostEditor({
9891034
hiddenFields: form.querySelectorAll(
9901035
'input[type=hidden][name=csrf]'
@@ -1012,17 +1057,24 @@
10121057
const firt = ePost.dataset.firt;
10131058
const fEditHead = ePost.dataset.fedithead;
10141059
eBtnEdit.innerText = "Editing...";
10151060
fetchPost(fpid)
10161061
.then(artifact=>{
1017
- const ondone = (fpe)=>{
1018
- restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1062
+ const ondone = (fpe, response)=>{
1063
+ /* onsubmit() and ondiscard() callback */
10191064
//console.debug("ondiscard/onsubmit", fpe, eToDisable);
10201065
if( fpe/*onsubmit*/ ){
1021
- if( fpe.widget.parentNode ){
1022
- fpe.widget.remove();
1066
+ if( fpid === response.uuid
1067
+ && !response.statusModified
1068
+ && 0===response.attachedCount ){
1069
+ fpe.reportError("No changes made.");
1070
+ }else{
1071
+ restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1072
+ window.location = F.repoUrl('forumpost/'+response.uuid);
10231073
}
1074
+ }else{
1075
+ restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
10241076
}
10251077
};
10261078
const eStatusSelect = ePost.querySelector(
10271079
':scope > fieldset.forum-status-selection select[name=status]'
10281080
);
@@ -1031,21 +1083,21 @@
10311083
hiddenFields: form.querySelectorAll('input[type=hidden]'),
10321084
ondiscard: ondone,
10331085
onsubmit: ondone,
10341086
draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
10351087
edit: artifact,
1036
- status: eStatusSelect?.value,
1088
+ //status: eStatusSelect?.value,
10371089
inReplyTo: firt
10381090
});
10391091
initFPEWidget(ePost, fpe);
10401092
});
10411093
}/*editClicked()*/;
10421094
10431095
document.body.querySelectorAll(
10441096
'.forumpost-single-controls > form'
10451097
).forEach(form=>{
1046
- //console.debug("Checking form",form);
1098
+ /* For each forum post... */
10471099
const eToDisable = [];
10481100
const eThePost = form.parentElement.parentElement;
10491101
if( !eThePost?.dataset?.fpid ){
10501102
console.warn("Unexpected missing fpid", eThePost);
10511103
return;
10521104
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -41,16 +41,21 @@
41 lose focus. If old state is found, the form is pre-populated
42 from it. The state is cleared on a successful submit.
43
44 opt.ondiscard[=function]: if set, a Discard button is added
45 which, when activated, clears the current draft and removes
46 this object's widget from the DOM. After that, opt.ondiscard()
47 is called and passed no arguments.
 
48
49 opt.onsubmit[=function]: if set, this function is called
50 immediately after the post has been successfully saved,
51 and passed this object.
 
 
 
 
52
53 opt.hiddenFields: an optional list of input elements to
54 incorporate into the form for requests which request the
55 preview or save the post.
56
@@ -152,10 +157,13 @@
152 onconfirm: ()=>this.discard()
153 });
154 }
155 }else{
156 e.button.submit.addEventListener('click', ()=>this.#submit());
 
 
 
157 }
158 e.button.submit.setAttribute('disabled', '');
159 wrapper.append(e.buttons);
160
161 e.error = D.addClass(D.div(), 'error', 'hidden');
@@ -249,11 +257,17 @@
249 }
250 this.#tabs.addTab(e.debug);
251 }
252 e.buttons.append(e.mimetype.wrapper);
253
254 if( opt.edit
 
 
 
 
 
 
255 && !opt.inReplyTo
256 && F.config.forumStatuses?.length>0 ){
257 /* Status selection. We probably don't _really_ want this in
258 the editor because people will open the editor, change the
259 status, and tap submit, resulting in a whole new, unedited
@@ -372,10 +386,14 @@
372 e.buttons.append(e.button.toggleHeader);
373 }
374
375 }/*constructor*/
376
 
 
 
 
377 close(){
378 const e = this.#e.widget;
379 if( e?.parentNode ){
380 if( this.#opt.onclose instanceof Function ){
381 try{this.#opt.onclose();}
@@ -387,10 +405,16 @@
387 e.classList.add('animate-exit');
388 e.addEventListener('animationend', ()=>e.remove(), {once: true});
389 }
390 }
391
 
 
 
 
 
 
392 discard(){
393 if( this.#opt.ondiscard instanceof Function ){
394 try{this.#opt.ondiscard();}
395 catch(e){
396 console.error("ForumPostEditor.ondiscard() threw:",e);
@@ -426,11 +450,15 @@
426 const e = this.#e.error;
427 D.clearElement(e);
428 if( msg.length ){
429 console.error('ForumPostEditor:',...msg);
430 e.classList.remove('hidden');
431 e.append(...msg);
 
 
 
 
432 }else{
433 e.classList.add('hidden');
434 }
435 }
436
@@ -654,16 +682,32 @@
654 return;
655 }
656 if( 1 ){
657 this.#clearDraft();
658 if( this.#opt.onsubmit instanceof Function ){
659 try{this.#opt.onsubmit(this);}
660 catch(e){
661 console.error("ForumPostEditor.onsubmit() threw: ", e);
662 }
663 }
664 window.location = F.repoUrl('forumpost/'+j.uuid);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665 }else{
666 this.reportError(
667 "Saving worked but we're ignoring it and staying here."
668 );
669 }
@@ -896,12 +940,10 @@
896 });
897 });
898 });
899 }
900
901 F.user.isIndividual = ['anonymous','nobody'].indexOf(F.user.name)<0;
902
903 /* Page-specific style tweaks for a ForumPostEditor instance. */
904 const initFPEWidget = (ePost, fpe)=>{
905 const w = fpe.widget;
906 w.style.borderTop = '1px dotted';
907 //w.style.marginTop = '0.35em';
@@ -928,10 +970,13 @@
928 const fpe = new F.ForumPostEditor({
929 draftKey: 'draft-forumnew',
930 hiddenFields: eForumNew.querySelectorAll('input[type=hidden]'),
931 ondiscard: ()=>{
932 window.location = F.repoUrl('forum');
 
 
 
933 }
934 });
935 eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
936 eForumNew.remove();
937 fossil.page.fpe = fpe /* for testing via the console */;
@@ -974,17 +1019,17 @@
974
975 const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{
976 const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable);
977 const fEditHead = ePost.dataset.fedithead;
978 eBtnReply.innerText = "Replying...";
979 const ondone = (fpe)=>{
 
980 restoreEditReplyElement(ePost, eBtnReply, eToDisable);
981 //console.debug("ondiscard/onsubmit", fpe, eToDisable);
982 if( fpe/*onsubmit*/ ){
983 if( fpe.widget.parentNode ){
984 fpe.widget.remove();
985 }
986 }
987 };
988 const fpe = new F.ForumPostEditor({
989 hiddenFields: form.querySelectorAll(
990 'input[type=hidden][name=csrf]'
@@ -1012,17 +1057,24 @@
1012 const firt = ePost.dataset.firt;
1013 const fEditHead = ePost.dataset.fedithead;
1014 eBtnEdit.innerText = "Editing...";
1015 fetchPost(fpid)
1016 .then(artifact=>{
1017 const ondone = (fpe)=>{
1018 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1019 //console.debug("ondiscard/onsubmit", fpe, eToDisable);
1020 if( fpe/*onsubmit*/ ){
1021 if( fpe.widget.parentNode ){
1022 fpe.widget.remove();
 
 
 
 
 
1023 }
 
 
1024 }
1025 };
1026 const eStatusSelect = ePost.querySelector(
1027 ':scope > fieldset.forum-status-selection select[name=status]'
1028 );
@@ -1031,21 +1083,21 @@
1031 hiddenFields: form.querySelectorAll('input[type=hidden]'),
1032 ondiscard: ondone,
1033 onsubmit: ondone,
1034 draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
1035 edit: artifact,
1036 status: eStatusSelect?.value,
1037 inReplyTo: firt
1038 });
1039 initFPEWidget(ePost, fpe);
1040 });
1041 }/*editClicked()*/;
1042
1043 document.body.querySelectorAll(
1044 '.forumpost-single-controls > form'
1045 ).forEach(form=>{
1046 //console.debug("Checking form",form);
1047 const eToDisable = [];
1048 const eThePost = form.parentElement.parentElement;
1049 if( !eThePost?.dataset?.fpid ){
1050 console.warn("Unexpected missing fpid", eThePost);
1051 return;
1052
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -41,16 +41,21 @@
41 lose focus. If old state is found, the form is pre-populated
42 from it. The state is cleared on a successful submit.
43
44 opt.ondiscard[=function]: if set, a Discard button is added
45 which, when activated, clears the current draft and removes
46 this object's widget from the DOM. After doing so,
47 opt.ondiscard() is called and passed no arguments. Exceptions
48 thrown by ondiscard() are ignored but may be logged.
49
50 opt.onsubmit[=function]: if set, this function is called
51 immediately after the post has been successfully saved, and
52 passed this object and a JSON-format response object from the
53 save request. On a successful edit, if the artifact was
54 actually modified then submission will redirect the page to
55 /forumpost/THE_POST_ID. Exceptions thrown by onsubmit()
56 are ignored but may be logged.
57
58 opt.hiddenFields: an optional list of input elements to
59 incorporate into the form for requests which request the
60 preview or save the post.
61
@@ -152,10 +157,13 @@
157 onconfirm: ()=>this.discard()
158 });
159 }
160 }else{
161 e.button.submit.addEventListener('click', ()=>this.#submit());
162 if( e.button.discard ){
163 e.button.submit.addEventListener('click', ()=>this.discard());
164 }
165 }
166 e.button.submit.setAttribute('disabled', '');
167 wrapper.append(e.buttons);
168
169 e.error = D.addClass(D.div(), 'error', 'hidden');
@@ -249,11 +257,17 @@
257 }
258 this.#tabs.addTab(e.debug);
259 }
260 e.buttons.append(e.mimetype.wrapper);
261
262 if( 0 /* 2026-06-09: disabling the status selection to keep
263 people from using the editor to change just that,
264 because doing so leaves us in a staet where we know
265 whether or not a Submit modifies the post, but not
266 whether or not out-of-band state like the status and
267 attachments were modified. */
268 && opt.edit
269 && !opt.inReplyTo
270 && F.config.forumStatuses?.length>0 ){
271 /* Status selection. We probably don't _really_ want this in
272 the editor because people will open the editor, change the
273 status, and tap submit, resulting in a whole new, unedited
@@ -372,10 +386,14 @@
386 e.buttons.append(e.button.toggleHeader);
387 }
388
389 }/*constructor*/
390
391 /*
392 ** Removes this object from the DOM. It has no side effects if
393 ** it's not in the DOM.
394 */
395 close(){
396 const e = this.#e.widget;
397 if( e?.parentNode ){
398 if( this.#opt.onclose instanceof Function ){
399 try{this.#opt.onclose();}
@@ -387,10 +405,16 @@
405 e.classList.add('animate-exit');
406 e.addEventListener('animationend', ()=>e.remove(), {once: true});
407 }
408 }
409
410 /*
411 ** Discards any draft edits then calls close(). If an ondiscard
412 ** callback was provided to the constructor then it is called
413 ** before the drafts are cleared and any exceptions it throws are
414 ** ignored (but may be logged).
415 */
416 discard(){
417 if( this.#opt.ondiscard instanceof Function ){
418 try{this.#opt.ondiscard();}
419 catch(e){
420 console.error("ForumPostEditor.ondiscard() threw:",e);
@@ -426,11 +450,15 @@
450 const e = this.#e.error;
451 D.clearElement(e);
452 if( msg.length ){
453 console.error('ForumPostEditor:',...msg);
454 e.classList.remove('hidden');
455 e.append(
456 ...msg, D.br(),
457 D.button("Clear", ()=>this.reportError())
458 /* Looks horrid in the Blitz skin */
459 );
460 }else{
461 e.classList.add('hidden');
462 }
463 }
464
@@ -654,16 +682,32 @@
682 return;
683 }
684 if( 1 ){
685 this.#clearDraft();
686 if( this.#opt.onsubmit instanceof Function ){
687 try{this.#opt.onsubmit(this, j);}
688 catch(e){
689 console.error("ForumPostEditor.onsubmit() threw: ", e);
690 }
691 }
692 /*
693 if( this.#opt.edit?.uuid === j.uuid ) then we know the
694 content did not change, but it's possible that attachments
695 and/or a status tag did. Ergo, we need to unconditionally
696 reload to render those changes (if any). The other option
697 is to tell the user "nothing changed" and leave them in
698 the editor, but that could be a lie because we don't know
699 if any attachments or tags were changed.
700 */
701 if( 0 ){
702 if( this.#opt.edit.uuid === j.uuid
703 && !j.statusModified && 0===j.attachedCount ){
704 this.reportError("No changes made.");
705 }else{
706 window.location = F.repoUrl('forumpost/'+j.uuid);
707 }
708 }
709 }else{
710 this.reportError(
711 "Saving worked but we're ignoring it and staying here."
712 );
713 }
@@ -896,12 +940,10 @@
940 });
941 });
942 });
943 }
944
 
 
945 /* Page-specific style tweaks for a ForumPostEditor instance. */
946 const initFPEWidget = (ePost, fpe)=>{
947 const w = fpe.widget;
948 w.style.borderTop = '1px dotted';
949 //w.style.marginTop = '0.35em';
@@ -928,10 +970,13 @@
970 const fpe = new F.ForumPostEditor({
971 draftKey: 'draft-forumnew',
972 hiddenFields: eForumNew.querySelectorAll('input[type=hidden]'),
973 ondiscard: ()=>{
974 window.location = F.repoUrl('forum');
975 },
976 onsubmit: (fpe, artifact)=>{
977 window.location = F.repoUrl('forumpost/'+artifact.uuid);
978 }
979 });
980 eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
981 eForumNew.remove();
982 fossil.page.fpe = fpe /* for testing via the console */;
@@ -974,17 +1019,17 @@
1019
1020 const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{
1021 const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable);
1022 const fEditHead = ePost.dataset.fedithead;
1023 eBtnReply.innerText = "Replying...";
1024 const ondone = (fpe, response)=>{
1025 /* onsubmit() and ondiscard() callback */
1026 restoreEditReplyElement(ePost, eBtnReply, eToDisable);
1027 //console.debug("ondiscard/onsubmit", fpe, artifact);
1028 if( response/*onsubmit*/ ){
1029 window.location = F.repoUrl('forumpost/'+response.uuid);
1030 fpe.close();
 
1031 }
1032 };
1033 const fpe = new F.ForumPostEditor({
1034 hiddenFields: form.querySelectorAll(
1035 'input[type=hidden][name=csrf]'
@@ -1012,17 +1057,24 @@
1057 const firt = ePost.dataset.firt;
1058 const fEditHead = ePost.dataset.fedithead;
1059 eBtnEdit.innerText = "Editing...";
1060 fetchPost(fpid)
1061 .then(artifact=>{
1062 const ondone = (fpe, response)=>{
1063 /* onsubmit() and ondiscard() callback */
1064 //console.debug("ondiscard/onsubmit", fpe, eToDisable);
1065 if( fpe/*onsubmit*/ ){
1066 if( fpid === response.uuid
1067 && !response.statusModified
1068 && 0===response.attachedCount ){
1069 fpe.reportError("No changes made.");
1070 }else{
1071 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1072 window.location = F.repoUrl('forumpost/'+response.uuid);
1073 }
1074 }else{
1075 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
1076 }
1077 };
1078 const eStatusSelect = ePost.querySelector(
1079 ':scope > fieldset.forum-status-selection select[name=status]'
1080 );
@@ -1031,21 +1083,21 @@
1083 hiddenFields: form.querySelectorAll('input[type=hidden]'),
1084 ondiscard: ondone,
1085 onsubmit: ondone,
1086 draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
1087 edit: artifact,
1088 //status: eStatusSelect?.value,
1089 inReplyTo: firt
1090 });
1091 initFPEWidget(ePost, fpe);
1092 });
1093 }/*editClicked()*/;
1094
1095 document.body.querySelectorAll(
1096 '.forumpost-single-controls > form'
1097 ).forEach(form=>{
1098 /* For each forum post... */
1099 const eToDisable = [];
1100 const eThePost = form.parentElement.parentElement;
1101 if( !eThePost?.dataset?.fpid ){
1102 console.warn("Unexpected missing fpid", eThePost);
1103 return;
1104
--- src/style.forum.css
+++ src/style.forum.css
@@ -90,10 +90,15 @@
9090
padding: 0.1em;
9191
}
9292
.ForumPostEditor > .buttons > label > input[type=checkbox]{
9393
cursor: pointer;
9494
}
95
+
96
+.ForumPostEditor > .error {
97
+ padding: 0.5em;
98
+ border-radius: 0.5em;
99
+}
95100
96101
.forumTime {
97102
/* Animate the transition when a ForumPostEditor shifts
98103
its post to the left. */
99104
transition: margin-left 0.25s ease-in-out;
100105
--- src/style.forum.css
+++ src/style.forum.css
@@ -90,10 +90,15 @@
90 padding: 0.1em;
91 }
92 .ForumPostEditor > .buttons > label > input[type=checkbox]{
93 cursor: pointer;
94 }
 
 
 
 
 
95
96 .forumTime {
97 /* Animate the transition when a ForumPostEditor shifts
98 its post to the left. */
99 transition: margin-left 0.25s ease-in-out;
100
--- src/style.forum.css
+++ src/style.forum.css
@@ -90,10 +90,15 @@
90 padding: 0.1em;
91 }
92 .ForumPostEditor > .buttons > label > input[type=checkbox]{
93 cursor: pointer;
94 }
95
96 .ForumPostEditor > .error {
97 padding: 0.5em;
98 border-radius: 0.5em;
99 }
100
101 .forumTime {
102 /* Animate the transition when a ForumPostEditor shifts
103 its post to the left. */
104 transition: margin-left 0.25s ease-in-out;
105

Keyboard Shortcuts

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