Fossil SCM

Elide empty/phantom (deleted) attachments from forum post attachment list. Hook status selection into the editor, with the caveat that changing ONLY the status via the editor will save a new copy of the post (identical to the previous one) so that should be done using the status selection widget shown in the post instead (which is now disabled while the editor is open).

stephan 2026-06-08 10:58 UTC forum-editor-2026
Commit 344c64e271a752b35a84f4db7725b85a6db419fcda1595bb15a46ea42f82a6e9
+9 -1
--- src/attach.c
+++ src/attach.c
@@ -906,11 +906,13 @@
906906
** output.
907907
**
908908
** ACHTUNG: if zTarget is a forum post, it "really should" be the ID
909909
** of the first version of that post, as that's where attachments are
910910
** intended to be applied so that they can be found and removed
911
-** consistently.
911
+** consistently. Potential TODO is have this function do that if
912
+** attachment_target_type(zTarget,1)!=0 but it would (for current
913
+** uses) require duplicating work already done in the callers.
912914
*/
913915
int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){
914916
int i;
915917
int rc = 0;
916918
int n = 0;
@@ -920,10 +922,11 @@
920922
char aKeyName[30]; /* Buffer for key "file%d:filename" */
921923
char aKeyDesc[30]; /* Buffer for key "file%d_desc" */
922924
923925
db_begin_transaction();
924926
szLimit = db_get_int("attachment-size-limit", 0);
927
+
925928
for(i = 1; ; ++i, ++n){
926929
/* Look for P("fileN"), where N=1..n */
927930
const char *zContent;
928931
const char *zFilename;
929932
int szContent;
@@ -1401,10 +1404,11 @@
14011404
#define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */
14021405
#define ATTACHLIST_SIZE 0x04 /* add size */
14031406
#define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */
14041407
#define ATTACHLIST_DETAILS_CLOSED 0x10 /* Wrap in a closed DETAILS element */
14051408
#define ATTACHLIST_DETAILS_OPEN 0x20 /* Wrap in an open DETAILS element */
1409
+#define ATTACHLIST_HIDE_EMPTY 0x40 /* Skip if size<1 */
14061410
#endif
14071411
14081412
/*
14091413
** Output HTML to show a list of attachments.
14101414
*/
@@ -1446,10 +1450,14 @@
14461450
const int sz = db_column_int(&q, 7);
14471451
if( (flags & ATTACHLIST_HIDE_UNAPPROVED)
14481452
&& moderation_pending(aid)
14491453
&& !moderation_user_could(aid, 1, 0) ){
14501454
continue;
1455
+ }
1456
+ if( sz<1 && (flags & ATTACHLIST_HIDE_EMPTY) ){
1457
+ /* Deleted or phantom items. */
1458
+ continue;
14511459
}
14521460
if( cnt==0 ){
14531461
if( bUseDetail ){
14541462
@ <details class='attachlist'
14551463
if( ATTACHLIST_DETAILS_OPEN & flags ){
14561464
--- src/attach.c
+++ src/attach.c
@@ -906,11 +906,13 @@
906 ** output.
907 **
908 ** ACHTUNG: if zTarget is a forum post, it "really should" be the ID
909 ** of the first version of that post, as that's where attachments are
910 ** intended to be applied so that they can be found and removed
911 ** consistently.
 
 
912 */
913 int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){
914 int i;
915 int rc = 0;
916 int n = 0;
@@ -920,10 +922,11 @@
920 char aKeyName[30]; /* Buffer for key "file%d:filename" */
921 char aKeyDesc[30]; /* Buffer for key "file%d_desc" */
922
923 db_begin_transaction();
924 szLimit = db_get_int("attachment-size-limit", 0);
 
925 for(i = 1; ; ++i, ++n){
926 /* Look for P("fileN"), where N=1..n */
927 const char *zContent;
928 const char *zFilename;
929 int szContent;
@@ -1401,10 +1404,11 @@
1401 #define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */
1402 #define ATTACHLIST_SIZE 0x04 /* add size */
1403 #define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */
1404 #define ATTACHLIST_DETAILS_CLOSED 0x10 /* Wrap in a closed DETAILS element */
1405 #define ATTACHLIST_DETAILS_OPEN 0x20 /* Wrap in an open DETAILS element */
 
1406 #endif
1407
1408 /*
1409 ** Output HTML to show a list of attachments.
1410 */
@@ -1446,10 +1450,14 @@
1446 const int sz = db_column_int(&q, 7);
1447 if( (flags & ATTACHLIST_HIDE_UNAPPROVED)
1448 && moderation_pending(aid)
1449 && !moderation_user_could(aid, 1, 0) ){
1450 continue;
 
 
 
 
1451 }
1452 if( cnt==0 ){
1453 if( bUseDetail ){
1454 @ <details class='attachlist'
1455 if( ATTACHLIST_DETAILS_OPEN & flags ){
1456
--- src/attach.c
+++ src/attach.c
@@ -906,11 +906,13 @@
906 ** output.
907 **
908 ** ACHTUNG: if zTarget is a forum post, it "really should" be the ID
909 ** of the first version of that post, as that's where attachments are
910 ** intended to be applied so that they can be found and removed
911 ** consistently. Potential TODO is have this function do that if
912 ** attachment_target_type(zTarget,1)!=0 but it would (for current
913 ** uses) require duplicating work already done in the callers.
914 */
915 int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){
916 int i;
917 int rc = 0;
918 int n = 0;
@@ -920,10 +922,11 @@
922 char aKeyName[30]; /* Buffer for key "file%d:filename" */
923 char aKeyDesc[30]; /* Buffer for key "file%d_desc" */
924
925 db_begin_transaction();
926 szLimit = db_get_int("attachment-size-limit", 0);
927
928 for(i = 1; ; ++i, ++n){
929 /* Look for P("fileN"), where N=1..n */
930 const char *zContent;
931 const char *zFilename;
932 int szContent;
@@ -1401,10 +1404,11 @@
1404 #define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */
1405 #define ATTACHLIST_SIZE 0x04 /* add size */
1406 #define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */
1407 #define ATTACHLIST_DETAILS_CLOSED 0x10 /* Wrap in a closed DETAILS element */
1408 #define ATTACHLIST_DETAILS_OPEN 0x20 /* Wrap in an open DETAILS element */
1409 #define ATTACHLIST_HIDE_EMPTY 0x40 /* Skip if size<1 */
1410 #endif
1411
1412 /*
1413 ** Output HTML to show a list of attachments.
1414 */
@@ -1446,10 +1450,14 @@
1450 const int sz = db_column_int(&q, 7);
1451 if( (flags & ATTACHLIST_HIDE_UNAPPROVED)
1452 && moderation_pending(aid)
1453 && !moderation_user_could(aid, 1, 0) ){
1454 continue;
1455 }
1456 if( sz<1 && (flags & ATTACHLIST_HIDE_EMPTY) ){
1457 /* Deleted or phantom items. */
1458 continue;
1459 }
1460 if( cnt==0 ){
1461 if( bUseDetail ){
1462 @ <details class='attachlist'
1463 if( ATTACHLIST_DETAILS_OPEN & flags ){
1464
+17 -7
--- src/forum.c
+++ src/forum.c
@@ -898,11 +898,18 @@
898898
break;
899899
}
900900
}
901901
if( !sCurrent ) sCurrent = &fss->aStatus[0];
902902
assert( sCurrent );
903
- @ <span class='forum-status-selection'>
903
+ @ <fieldset class='forum-status-selection'>\
904
+ @ <legend>Status \
905
+ @ <span class='help-buttonlet'>\
906
+ @ Moderators and post owners 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>\
904911
if( forum_may_set_status(fp->fpid) ){
905912
@ <form method="post" action='%R/forumpost_status'>
906913
login_insert_csrf_secret();
907914
@ <input type='hidden' name='fpid' value='%s(fp->zUuid)' />
908915
@ <select name='status' data-fpid='%s(fp->zUuid)'\
@@ -921,11 +928,11 @@
921928
@ </form>
922929
/* Form is activated in fossil.page.forumpost.js */
923930
}else{
924931
@ <button disabled>Status: %h(sCurrent->zLabel)</button>
925932
}
926
- @ </span>
933
+ @ </fieldset>
927934
fossil_free(zCurrent);
928935
}
929936
}
930937
931938
/*
@@ -1053,18 +1060,20 @@
10531060
static void forum_render_attachment_list(const char *zUuid){
10541061
#if 1
10551062
attachment_list(zUuid, "&#x1f4ce; Attachments", 0
10561063
| ATTACHLIST_SIZE
10571064
| ATTACHLIST_HIDE_UNAPPROVED
1058
- | ATTACHLIST_DETAILS_CLOSED);
1065
+ | ATTACHLIST_DETAILS_CLOSED
1066
+ | ATTACHLIST_HIDE_EMPTY);
10591067
#else
10601068
char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%!S'>"
10611069
"Attachments</a>:", zUuid);
10621070
attachment_list(zUuid, zLbl,
10631071
ATTACHLIST_HRULE_ABOVE
10641072
| ATTACHLIST_SIZE
1065
- | ATTACHLIST_HIDE_UNAPPROVED);
1073
+ | ATTACHLIST_HIDE_UNAPPROVED
1074
+ | ATTACHLIST_HIDE_EMPTY);
10661075
fossil_free(zLbl);
10671076
#endif
10681077
}
10691078
10701079
/*
@@ -3064,20 +3073,21 @@
30643073
"\"iPostFlags\":%d}\n", iPostFlags);
30653074
}
30663075
goto ajax_save_end;
30673076
}else{
30683077
const int bNeedsModeration = forum_need_moderation();
3069
- const int fpRoot = forumpost_head_rid(nrid);
3078
+ const int fpHead = forumpost_head_rid(nrid);
30703079
assert( nrid>0 );
3080
+ assert( fpHead>0 );
30713081
zNewUuid = rid_to_uuid(nrid);
30723082
if( 0!=P("file1") ){
30733083
/* Attachments */
30743084
if( !g.perm.Admin && !g.perm.AttachForum ){
30753085
rc = -ajax_route_error(403, "No permission no attach files.");
30763086
goto ajax_save_end;
30773087
}else{
3078
- char *zRoot = (nrid==fpRoot) ? 0 : rid_to_uuid(fpRoot);
3088
+ char *zRoot = (nrid==fpHead) ? 0 : rid_to_uuid(fpHead);
30793089
const int atRc =
30803090
attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
30813091
bNeedsModeration);
30823092
fossil_free(zRoot);
30833093
if( atRc<0 ){
@@ -3098,11 +3108,11 @@
30983108
" UNION ALL\n"
30993109
" SELECT 'f'||a.attachid FROM blob b, attachment a\n"
31003110
" WHERE b.rid=%d\n"
31013111
" AND b.uuid=a.target\n"
31023112
") DELETE FROM pending_alert WHERE eventid IN x",
3103
- fpRoot, fpRoot
3113
+ fpHead, fpHead
31043114
);
31053115
}
31063116
}
31073117
}
31083118
if( 0==bNeedsModeration
31093119
--- src/forum.c
+++ src/forum.c
@@ -898,11 +898,18 @@
898 break;
899 }
900 }
901 if( !sCurrent ) sCurrent = &fss->aStatus[0];
902 assert( sCurrent );
903 @ <span class='forum-status-selection'>
 
 
 
 
 
 
 
904 if( forum_may_set_status(fp->fpid) ){
905 @ <form method="post" action='%R/forumpost_status'>
906 login_insert_csrf_secret();
907 @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' />
908 @ <select name='status' data-fpid='%s(fp->zUuid)'\
@@ -921,11 +928,11 @@
921 @ </form>
922 /* Form is activated in fossil.page.forumpost.js */
923 }else{
924 @ <button disabled>Status: %h(sCurrent->zLabel)</button>
925 }
926 @ </span>
927 fossil_free(zCurrent);
928 }
929 }
930
931 /*
@@ -1053,18 +1060,20 @@
1053 static void forum_render_attachment_list(const char *zUuid){
1054 #if 1
1055 attachment_list(zUuid, "&#x1f4ce; Attachments", 0
1056 | ATTACHLIST_SIZE
1057 | ATTACHLIST_HIDE_UNAPPROVED
1058 | ATTACHLIST_DETAILS_CLOSED);
 
1059 #else
1060 char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%!S'>"
1061 "Attachments</a>:", zUuid);
1062 attachment_list(zUuid, zLbl,
1063 ATTACHLIST_HRULE_ABOVE
1064 | ATTACHLIST_SIZE
1065 | ATTACHLIST_HIDE_UNAPPROVED);
 
1066 fossil_free(zLbl);
1067 #endif
1068 }
1069
1070 /*
@@ -3064,20 +3073,21 @@
3064 "\"iPostFlags\":%d}\n", iPostFlags);
3065 }
3066 goto ajax_save_end;
3067 }else{
3068 const int bNeedsModeration = forum_need_moderation();
3069 const int fpRoot = forumpost_head_rid(nrid);
3070 assert( nrid>0 );
 
3071 zNewUuid = rid_to_uuid(nrid);
3072 if( 0!=P("file1") ){
3073 /* Attachments */
3074 if( !g.perm.Admin && !g.perm.AttachForum ){
3075 rc = -ajax_route_error(403, "No permission no attach files.");
3076 goto ajax_save_end;
3077 }else{
3078 char *zRoot = (nrid==fpRoot) ? 0 : rid_to_uuid(fpRoot);
3079 const int atRc =
3080 attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3081 bNeedsModeration);
3082 fossil_free(zRoot);
3083 if( atRc<0 ){
@@ -3098,11 +3108,11 @@
3098 " UNION ALL\n"
3099 " SELECT 'f'||a.attachid FROM blob b, attachment a\n"
3100 " WHERE b.rid=%d\n"
3101 " AND b.uuid=a.target\n"
3102 ") DELETE FROM pending_alert WHERE eventid IN x",
3103 fpRoot, fpRoot
3104 );
3105 }
3106 }
3107 }
3108 if( 0==bNeedsModeration
3109
--- src/forum.c
+++ src/forum.c
@@ -898,11 +898,18 @@
898 break;
899 }
900 }
901 if( !sCurrent ) sCurrent = &fss->aStatus[0];
902 assert( sCurrent );
903 @ <fieldset class='forum-status-selection'>\
904 @ <legend>Status \
905 @ <span class='help-buttonlet'>\
906 @ Moderators and post owners 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'>
913 login_insert_csrf_secret();
914 @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' />
915 @ <select name='status' data-fpid='%s(fp->zUuid)'\
@@ -921,11 +928,11 @@
928 @ </form>
929 /* Form is activated in fossil.page.forumpost.js */
930 }else{
931 @ <button disabled>Status: %h(sCurrent->zLabel)</button>
932 }
933 @ </fieldset>
934 fossil_free(zCurrent);
935 }
936 }
937
938 /*
@@ -1053,18 +1060,20 @@
1060 static void forum_render_attachment_list(const char *zUuid){
1061 #if 1
1062 attachment_list(zUuid, "&#x1f4ce; Attachments", 0
1063 | ATTACHLIST_SIZE
1064 | ATTACHLIST_HIDE_UNAPPROVED
1065 | ATTACHLIST_DETAILS_CLOSED
1066 | ATTACHLIST_HIDE_EMPTY);
1067 #else
1068 char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%!S'>"
1069 "Attachments</a>:", zUuid);
1070 attachment_list(zUuid, zLbl,
1071 ATTACHLIST_HRULE_ABOVE
1072 | ATTACHLIST_SIZE
1073 | ATTACHLIST_HIDE_UNAPPROVED
1074 | ATTACHLIST_HIDE_EMPTY);
1075 fossil_free(zLbl);
1076 #endif
1077 }
1078
1079 /*
@@ -3064,20 +3073,21 @@
3073 "\"iPostFlags\":%d}\n", iPostFlags);
3074 }
3075 goto ajax_save_end;
3076 }else{
3077 const int bNeedsModeration = forum_need_moderation();
3078 const int fpHead = forumpost_head_rid(nrid);
3079 assert( nrid>0 );
3080 assert( fpHead>0 );
3081 zNewUuid = rid_to_uuid(nrid);
3082 if( 0!=P("file1") ){
3083 /* Attachments */
3084 if( !g.perm.Admin && !g.perm.AttachForum ){
3085 rc = -ajax_route_error(403, "No permission no attach files.");
3086 goto ajax_save_end;
3087 }else{
3088 char *zRoot = (nrid==fpHead) ? 0 : rid_to_uuid(fpHead);
3089 const int atRc =
3090 attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3091 bNeedsModeration);
3092 fossil_free(zRoot);
3093 if( atRc<0 ){
@@ -3098,11 +3108,11 @@
3108 " UNION ALL\n"
3109 " SELECT 'f'||a.attachid FROM blob b, attachment a\n"
3110 " WHERE b.rid=%d\n"
3111 " AND b.uuid=a.target\n"
3112 ") DELETE FROM pending_alert WHERE eventid IN x",
3113 fpHead, fpHead
3114 );
3115 }
3116 }
3117 }
3118 if( 0==bNeedsModeration
3119
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -52,18 +52,19 @@
5252
5353
opt.hiddenFields: an optional list of input elements to
5454
incorporate into the form for requests which request the
5555
preview or save the post.
5656
57
- TODO:
58
-
5957
opt.inReplyTo=uuid: if this is a response to a post, this
6058
is the full forum post uuid of the being-replied-to post.
6159
6260
opt.edit=artifactObject: if this is an edit of an existing
6361
post, this is the full JSON-format artifact of the forum post
6462
the being-edited post, as returned by /ajax/artifact.json.
63
+
64
+ opt.status: optional current status tag value for opt.edit,
65
+ if known. This is used for pre-selecting a status value.
6566
*/
6667
constructor(opt){
6768
opt = this.#opt = F.nu({
6869
// todo: defaults once we determine the options
6970
// inReplyTo: hash
@@ -109,11 +110,11 @@
109110
{ /* Mimetype... */
110111
e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
111112
const sel = e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
112113
this.#toDisable.push(sel);
113114
let i = 0;
114
- D.option(sel, '', 'Markup format').disabled = true;
115
+ D.option(sel, '', '- Markup format -').disabled = true;
115116
for(const [k,v] of Object.entries({
116117
'text/x-markdown': 'Markdown',
117118
'text/x-fossil-wiki': 'Fossil Wiki',
118119
'text/plain': 'Plain text'
119120
})) {
@@ -209,11 +210,12 @@
209210
D.addClass(D.textarea(), 'editor'),
210211
'placeholder',
211212
'Your message to other forum-goers...'
212213
);
213214
e.tabEdit.append(e.editor);
214
- e.tabEdit.dataset.tabLabel = 'Edit';
215
+ e.tabEdit.dataset.tabLabel = (opt.edit || !opt.inReplyTo)
216
+ ? 'Edit' : 'Reply';
215217
this.#tabs.addTab( e.tabEdit );
216218
this.#tabs.switchToTab( e.tabEdit );
217219
if( this.#draft ){
218220
this.editorContent = this.#draft.content || opt.edit?.W || '';
219221
e.editor.addEventListener(
@@ -247,11 +249,13 @@
247249
}
248250
this.#tabs.addTab(e.debug);
249251
}
250252
e.buttons.append(e.mimetype.wrapper);
251253
252
- if( 0 && F.config.forumStatuses?.length>0 ){
254
+ if( opt.edit
255
+ && !opt.inReplyTo
256
+ && F.config.forumStatuses?.length>0 ){
253257
/* Status selection. We probably don't _really_ want this in
254258
the editor because people will open the editor, change the
255259
status, and tap submit, resulting in a whole new, unedited
256260
copy of the post, differing only in the new 'status' tag
257261
added to it. */
@@ -448,10 +452,18 @@
448452
D.append(
449453
D.li(list),
450454
D.attr(D.a(F.repoUrl('markup_help'), 'Markup styles'),
451455
'target', '_new')
452456
);
457
+ if( this.#e.status ){
458
+ D.append(
459
+ D.li(list),
460
+ "Trip: to change just the status, use the widget which appears in ",
461
+ "the post, not the editor. That will save only a single tag instead of ",
462
+ "a new edit of the post."
463
+ );
464
+ }
453465
eh.append(list);
454466
}
455467
456468
#initAttacherTab(){
457469
this.#att = new F.Attacher({
@@ -585,11 +597,11 @@
585597
if( this.#e.status ){
586598
/* Send the status only if it was modified, otherwise we may
587599
add a superfluous tag. */
588600
const v = this.#e.status.value;
589601
if( this.#e.status.dataset.originalValue !== v ){
590
- fd.append( "status", v );
602
+ fd.append("status", v);
591603
}
592604
}
593605
if( e.debug ){
594606
e.debug.querySelectorAll('input[type=checkbox]').forEach(cb=>{
595607
if( cb.checked ){
@@ -978,17 +990,22 @@
978990
if( fpe.widget.parentNode ){
979991
fpe.widget.remove();
980992
}
981993
}
982994
};
995
+ const eStatusSelect = ePost.querySelector(
996
+ ':scope > fieldset.forum-status-selection select[name=status]'
997
+ );
998
+
983999
const fpe = new F.ForumPostEditor({
9841000
hiddenFields: form.querySelectorAll('input[type=hidden]'),
9851001
ondiscard: ondone,
9861002
onsubmit: ondone,
9871003
draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
9881004
hideTitle: true/*fixme: only show if this is the root post*/,
9891005
edit: artifact,
1006
+ status: eStatusSelect?.value,
9901007
inReplyTo: firt
9911008
});
9921009
const w = fpe.widget;
9931010
w.style.borderTop = '2px dotted';
9941011
/* Adding an "Editing..." <h3> here adds way too much space */
@@ -1023,14 +1040,24 @@
10231040
b.type = 'button';
10241041
eToDisable.push(b);
10251042
btnEdit.parentElement.insertBefore(b, btnEdit);
10261043
btnEdit.remove();
10271044
}
1045
+ /* Problem: we really need to disable the status-selection
1046
+ button because it would redirect mid-edit. We wouldn't lose
1047
+ the edits but would lose any pending attachments. We work
1048
+ around this by disabling this selection and adding a new
1049
+ status selection widget in the editor, inheriting this
1050
+ one's value. */
1051
+ const eStatusChange = eThePost.querySelector(
1052
+ ':scope > fieldset.forum-status-selection'
1053
+ );
1054
+ if( eStatusChange ) eToDisable.push(eStatusChange);
10281055
})/*for-each form*/;
10291056
}/* /forumpost and /forumthread */
10301057
10311058
if( Date.now() % 17 === 0 ){
10321059
/* Purge old drafts only every now and then. */
10331060
F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/);
10341061
}
10351062
})/*F.onPageLoad callback*/;
10361063
})(window.fossil);
10371064
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -52,18 +52,19 @@
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
57 TODO:
58
59 opt.inReplyTo=uuid: if this is a response to a post, this
60 is the full forum post uuid of the being-replied-to post.
61
62 opt.edit=artifactObject: if this is an edit of an existing
63 post, this is the full JSON-format artifact of the forum post
64 the being-edited post, as returned by /ajax/artifact.json.
 
 
 
65 */
66 constructor(opt){
67 opt = this.#opt = F.nu({
68 // todo: defaults once we determine the options
69 // inReplyTo: hash
@@ -109,11 +110,11 @@
109 { /* Mimetype... */
110 e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
111 const sel = e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
112 this.#toDisable.push(sel);
113 let i = 0;
114 D.option(sel, '', 'Markup format').disabled = true;
115 for(const [k,v] of Object.entries({
116 'text/x-markdown': 'Markdown',
117 'text/x-fossil-wiki': 'Fossil Wiki',
118 'text/plain': 'Plain text'
119 })) {
@@ -209,11 +210,12 @@
209 D.addClass(D.textarea(), 'editor'),
210 'placeholder',
211 'Your message to other forum-goers...'
212 );
213 e.tabEdit.append(e.editor);
214 e.tabEdit.dataset.tabLabel = 'Edit';
 
215 this.#tabs.addTab( e.tabEdit );
216 this.#tabs.switchToTab( e.tabEdit );
217 if( this.#draft ){
218 this.editorContent = this.#draft.content || opt.edit?.W || '';
219 e.editor.addEventListener(
@@ -247,11 +249,13 @@
247 }
248 this.#tabs.addTab(e.debug);
249 }
250 e.buttons.append(e.mimetype.wrapper);
251
252 if( 0 && F.config.forumStatuses?.length>0 ){
 
 
253 /* Status selection. We probably don't _really_ want this in
254 the editor because people will open the editor, change the
255 status, and tap submit, resulting in a whole new, unedited
256 copy of the post, differing only in the new 'status' tag
257 added to it. */
@@ -448,10 +452,18 @@
448 D.append(
449 D.li(list),
450 D.attr(D.a(F.repoUrl('markup_help'), 'Markup styles'),
451 'target', '_new')
452 );
 
 
 
 
 
 
 
 
453 eh.append(list);
454 }
455
456 #initAttacherTab(){
457 this.#att = new F.Attacher({
@@ -585,11 +597,11 @@
585 if( this.#e.status ){
586 /* Send the status only if it was modified, otherwise we may
587 add a superfluous tag. */
588 const v = this.#e.status.value;
589 if( this.#e.status.dataset.originalValue !== v ){
590 fd.append( "status", v );
591 }
592 }
593 if( e.debug ){
594 e.debug.querySelectorAll('input[type=checkbox]').forEach(cb=>{
595 if( cb.checked ){
@@ -978,17 +990,22 @@
978 if( fpe.widget.parentNode ){
979 fpe.widget.remove();
980 }
981 }
982 };
 
 
 
 
983 const fpe = new F.ForumPostEditor({
984 hiddenFields: form.querySelectorAll('input[type=hidden]'),
985 ondiscard: ondone,
986 onsubmit: ondone,
987 draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
988 hideTitle: true/*fixme: only show if this is the root post*/,
989 edit: artifact,
 
990 inReplyTo: firt
991 });
992 const w = fpe.widget;
993 w.style.borderTop = '2px dotted';
994 /* Adding an "Editing..." <h3> here adds way too much space */
@@ -1023,14 +1040,24 @@
1023 b.type = 'button';
1024 eToDisable.push(b);
1025 btnEdit.parentElement.insertBefore(b, btnEdit);
1026 btnEdit.remove();
1027 }
 
 
 
 
 
 
 
 
 
 
1028 })/*for-each form*/;
1029 }/* /forumpost and /forumthread */
1030
1031 if( Date.now() % 17 === 0 ){
1032 /* Purge old drafts only every now and then. */
1033 F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/);
1034 }
1035 })/*F.onPageLoad callback*/;
1036 })(window.fossil);
1037
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -52,18 +52,19 @@
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
 
 
57 opt.inReplyTo=uuid: if this is a response to a post, this
58 is the full forum post uuid of the being-replied-to post.
59
60 opt.edit=artifactObject: if this is an edit of an existing
61 post, this is the full JSON-format artifact of the forum post
62 the being-edited post, as returned by /ajax/artifact.json.
63
64 opt.status: optional current status tag value for opt.edit,
65 if known. This is used for pre-selecting a status value.
66 */
67 constructor(opt){
68 opt = this.#opt = F.nu({
69 // todo: defaults once we determine the options
70 // inReplyTo: hash
@@ -109,11 +110,11 @@
110 { /* Mimetype... */
111 e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
112 const sel = e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
113 this.#toDisable.push(sel);
114 let i = 0;
115 D.option(sel, '', '- Markup format -').disabled = true;
116 for(const [k,v] of Object.entries({
117 'text/x-markdown': 'Markdown',
118 'text/x-fossil-wiki': 'Fossil Wiki',
119 'text/plain': 'Plain text'
120 })) {
@@ -209,11 +210,12 @@
210 D.addClass(D.textarea(), 'editor'),
211 'placeholder',
212 'Your message to other forum-goers...'
213 );
214 e.tabEdit.append(e.editor);
215 e.tabEdit.dataset.tabLabel = (opt.edit || !opt.inReplyTo)
216 ? 'Edit' : 'Reply';
217 this.#tabs.addTab( e.tabEdit );
218 this.#tabs.switchToTab( e.tabEdit );
219 if( this.#draft ){
220 this.editorContent = this.#draft.content || opt.edit?.W || '';
221 e.editor.addEventListener(
@@ -247,11 +249,13 @@
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
260 copy of the post, differing only in the new 'status' tag
261 added to it. */
@@ -448,10 +452,18 @@
452 D.append(
453 D.li(list),
454 D.attr(D.a(F.repoUrl('markup_help'), 'Markup styles'),
455 'target', '_new')
456 );
457 if( this.#e.status ){
458 D.append(
459 D.li(list),
460 "Trip: to change just the status, use the widget which appears in ",
461 "the post, not the editor. That will save only a single tag instead of ",
462 "a new edit of the post."
463 );
464 }
465 eh.append(list);
466 }
467
468 #initAttacherTab(){
469 this.#att = new F.Attacher({
@@ -585,11 +597,11 @@
597 if( this.#e.status ){
598 /* Send the status only if it was modified, otherwise we may
599 add a superfluous tag. */
600 const v = this.#e.status.value;
601 if( this.#e.status.dataset.originalValue !== v ){
602 fd.append("status", v);
603 }
604 }
605 if( e.debug ){
606 e.debug.querySelectorAll('input[type=checkbox]').forEach(cb=>{
607 if( cb.checked ){
@@ -978,17 +990,22 @@
990 if( fpe.widget.parentNode ){
991 fpe.widget.remove();
992 }
993 }
994 };
995 const eStatusSelect = ePost.querySelector(
996 ':scope > fieldset.forum-status-selection select[name=status]'
997 );
998
999 const fpe = new F.ForumPostEditor({
1000 hiddenFields: form.querySelectorAll('input[type=hidden]'),
1001 ondiscard: ondone,
1002 onsubmit: ondone,
1003 draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
1004 hideTitle: true/*fixme: only show if this is the root post*/,
1005 edit: artifact,
1006 status: eStatusSelect?.value,
1007 inReplyTo: firt
1008 });
1009 const w = fpe.widget;
1010 w.style.borderTop = '2px dotted';
1011 /* Adding an "Editing..." <h3> here adds way too much space */
@@ -1023,14 +1040,24 @@
1040 b.type = 'button';
1041 eToDisable.push(b);
1042 btnEdit.parentElement.insertBefore(b, btnEdit);
1043 btnEdit.remove();
1044 }
1045 /* Problem: we really need to disable the status-selection
1046 button because it would redirect mid-edit. We wouldn't lose
1047 the edits but would lose any pending attachments. We work
1048 around this by disabling this selection and adding a new
1049 status selection widget in the editor, inheriting this
1050 one's value. */
1051 const eStatusChange = eThePost.querySelector(
1052 ':scope > fieldset.forum-status-selection'
1053 );
1054 if( eStatusChange ) eToDisable.push(eStatusChange);
1055 })/*for-each form*/;
1056 }/* /forumpost and /forumthread */
1057
1058 if( Date.now() % 17 === 0 ){
1059 /* Purge old drafts only every now and then. */
1060 F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/);
1061 }
1062 })/*F.onPageLoad callback*/;
1063 })(window.fossil);
1064
--- src/style.forum.css
+++ src/style.forum.css
@@ -1,6 +1,12 @@
11
/* Styles specific to the forum family of pages */
2
+
3
+fieldset.forum-status-selection {
4
+ max-width: max-content;
5
+ border-radius: 0.5em;
6
+ padding: 0 0.5em;
7
+}
28
39
.ForumPostEditor {
410
display: flex;
511
flex-direction: column;
612
gap: 1em;
713
--- src/style.forum.css
+++ src/style.forum.css
@@ -1,6 +1,12 @@
1 /* Styles specific to the forum family of pages */
 
 
 
 
 
 
2
3 .ForumPostEditor {
4 display: flex;
5 flex-direction: column;
6 gap: 1em;
7
--- src/style.forum.css
+++ src/style.forum.css
@@ -1,6 +1,12 @@
1 /* Styles specific to the forum family of pages */
2
3 fieldset.forum-status-selection {
4 max-width: max-content;
5 border-radius: 0.5em;
6 padding: 0 0.5em;
7 }
8
9 .ForumPostEditor {
10 display: flex;
11 flex-direction: column;
12 gap: 1em;
13

Keyboard Shortcuts

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